no message
This commit is contained in:
@@ -44,6 +44,17 @@ void dbg_ignored(const char *what, uint32_t id) {
|
||||
}
|
||||
}
|
||||
|
||||
// Server GetDegreeFromPosition (game/src/vector.cpp): compass heading of a
|
||||
// server-frame direction, 0 = +Y, clockwise toward +X, in [0, 360). Same frame
|
||||
// as the decoded GC_MOVE bRot, i.e. Entity::angle.
|
||||
float heading_deg(float dx, float dy) {
|
||||
float deg = std::atan2(dx, dy) * (180.0f / 3.14159265f);
|
||||
if (deg < 0.0f) {
|
||||
deg += 360.0f;
|
||||
}
|
||||
return deg;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Entity &EntityStore::touch(uint32_t vid, bool &created) {
|
||||
@@ -240,7 +251,11 @@ void EntityStore::begin_state_walk(Entity &e, const StateCmd &c, uint8_t after_f
|
||||
if (dur == 0) {
|
||||
dur = 1; // never a zero-length walk; tick() finishes it next frame
|
||||
}
|
||||
e.angle = c.rot;
|
||||
// Reference MovementProcess faces a walking remote actor along Src->Dst
|
||||
// (SetAdvancingRotation from the pixel positions); the packet's bRot is only
|
||||
// applied once it stops (m_fDstRot). Server Follow() can send a bRot that
|
||||
// faces the victim while Dst is a flank point, so using bRot here slides.
|
||||
e.angle = dist > 0.0f ? heading_deg(dx, dy) : c.rot;
|
||||
e.moving = true;
|
||||
e.func = FUNC_MOVE;
|
||||
e.sx = e.x;
|
||||
@@ -3555,37 +3570,135 @@ void EntityStore::tick() {
|
||||
// CLIENT-GAP §3.2: release any TCP state commands that have come due, then
|
||||
// advance the walk. A queued command released here starts its walk this tick.
|
||||
process_states();
|
||||
uint32_t dt_ms = m_ticked && m_now > m_last_tick_ms ? m_now - m_last_tick_ms : 0;
|
||||
if (dt_ms > MOVE_MAX_TICK_MS) {
|
||||
dt_ms = MOVE_MAX_TICK_MS;
|
||||
}
|
||||
m_last_tick_ms = m_now;
|
||||
m_ticked = true;
|
||||
for (auto &kv : m_ents) {
|
||||
Entity &e = kv.second;
|
||||
if (!e.moving) {
|
||||
continue;
|
||||
}
|
||||
float t = e.move_dur_ms == 0
|
||||
? 1.0f
|
||||
: (float)(m_now - e.move_start_ms) / (float)e.move_dur_ms;
|
||||
if (t <= 0.0f) {
|
||||
t = 0.0f;
|
||||
const float speed = motion_move_speed(e);
|
||||
if (speed > 0.0f) {
|
||||
advance_walk_by_motion(e, speed, dt_ms);
|
||||
} else {
|
||||
advance_walk_by_duration(e);
|
||||
}
|
||||
if (t >= 1.0f) {
|
||||
e.x = e.tx;
|
||||
e.y = e.ty;
|
||||
}
|
||||
}
|
||||
|
||||
void EntityStore::set_motion_speed(uint32_t vid, float walk_cm_s, float run_cm_s) {
|
||||
auto it = m_ents.find(vid);
|
||||
if (it == m_ents.end()) {
|
||||
dbg_ignored("set_motion_speed", vid);
|
||||
return;
|
||||
}
|
||||
it->second.walk_motion_speed = walk_cm_s > 0.0f ? walk_cm_s : 0.0f;
|
||||
it->second.run_motion_speed = run_cm_s > 0.0f ? run_cm_s : 0.0f;
|
||||
}
|
||||
|
||||
// Reference CActorInstance::Move(): SetLoopMotion(m_isWalking ? WALK : RUN, 0.15,
|
||||
// m_fMovSpd) and CInstanceBase::SetMoveSpeed: m_fMovSpd = movSpd / 100 (> 1100 ->
|
||||
// 0). The walk then advances by that motion's accumulation. Deviations: movSpd 0
|
||||
// (not sent yet) counts as 100, and a zero factor (> 1100) falls back to the
|
||||
// dwDuration lerp instead of freezing the actor mid-walk. The pushed speeds are
|
||||
// the on-foot motions, so a mounted actor (horse motion set not measured) also
|
||||
// uses the lerp.
|
||||
float EntityStore::motion_move_speed(const Entity &e) {
|
||||
const float base = e.walk_mode == WALKMODE_WALK ? e.walk_motion_speed : e.run_motion_speed;
|
||||
if (base <= 0.0f || e.moving_speed > 1100 || e.mount_vnum != 0) {
|
||||
return 0.0f;
|
||||
}
|
||||
const float factor = e.moving_speed == 0 ? 1.0f : (float)e.moving_speed / 100.0f;
|
||||
return base * factor;
|
||||
}
|
||||
|
||||
// Reference m_kMovAfterFunc switch on arrival: COMBO / ATTACK / MOB_SKILL snap
|
||||
// to Dst and act; everything else snaps to Dst, faces m_fDstRot and stops.
|
||||
void EntityStore::finish_walk(Entity &e, bool snap_to_dst) {
|
||||
if (snap_to_dst) {
|
||||
e.x = e.tx;
|
||||
e.y = e.ty;
|
||||
}
|
||||
e.moving = false;
|
||||
e.skip_collision = false;
|
||||
if (e.mov_after_func == FUNC_COMBO || e.mov_after_func == FUNC_ATTACK ||
|
||||
e.mov_after_func == FUNC_MOB_SKILL) {
|
||||
e.func = e.mov_after_func;
|
||||
} else {
|
||||
e.func = FUNC_WAIT;
|
||||
}
|
||||
e.angle = e.mov_after_rot;
|
||||
e.mov_after_func = FUNC_WAIT;
|
||||
}
|
||||
|
||||
// Reference CInstanceBase::MovementProcess (non-main branch). Src/Dst are the
|
||||
// server-frame points from the state command; Cur advances along Src->Dst by
|
||||
// the motion speed and "arrives" on the frame whose step crosses Dst.
|
||||
void EntityStore::advance_walk_by_motion(Entity &e, float speed, uint32_t dt_ms) {
|
||||
float dir_x = e.tx - e.sx;
|
||||
float dir_y = e.ty - e.sy;
|
||||
float total = std::sqrt(dir_x * dir_x + dir_y * dir_y);
|
||||
if (total > 0.0f) {
|
||||
e.angle = heading_deg(dir_x, dir_y);
|
||||
dir_x /= total;
|
||||
dir_y /= total;
|
||||
}
|
||||
const float step = speed * (float)dt_ms / 1000.0f;
|
||||
const float next_x = e.x + dir_x * step;
|
||||
const float next_y = e.y + dir_y * step;
|
||||
const float cx = e.x - e.sx, cy = e.y - e.sy;
|
||||
const float nx = next_x - e.sx, ny = next_y - e.sy;
|
||||
const float cur_len = std::sqrt(cx * cx + cy * cy);
|
||||
const float next_len = std::sqrt(nx * nx + ny * ny);
|
||||
|
||||
if (total - cur_len < -MOVE_OVERSHOOT_CM) {
|
||||
// latency overran Dst: re-source here, turn back toward Dst, and stop at
|
||||
// the next arrival instead of carrying on with FUNC_MOVE.
|
||||
e.sx = e.x;
|
||||
e.sy = e.y;
|
||||
e.angle = heading_deg(e.tx - e.x, e.ty - e.y);
|
||||
if (e.mov_after_func == FUNC_MOVE) {
|
||||
e.mov_after_func = FUNC_WAIT;
|
||||
}
|
||||
} else if (cur_len <= total && total <= next_len) {
|
||||
if (e.dead || e.knock_down) {
|
||||
e.moving = false;
|
||||
e.skip_collision = false;
|
||||
// reference m_kMovAfterFunc: once the walk reaches Dst, run the
|
||||
// action the state packet asked for (COMBO / ATTACK / MOB_SKILL);
|
||||
// otherwise settle to WAIT.
|
||||
if (e.mov_after_func == FUNC_COMBO || e.mov_after_func == FUNC_ATTACK ||
|
||||
e.mov_after_func == FUNC_MOB_SKILL) {
|
||||
e.func = e.mov_after_func;
|
||||
e.angle = e.mov_after_rot;
|
||||
} else {
|
||||
e.func = FUNC_WAIT;
|
||||
}
|
||||
e.mov_after_func = FUNC_WAIT;
|
||||
} else {
|
||||
e.x = e.sx + (e.tx - e.sx) * t;
|
||||
e.y = e.sy + (e.ty - e.sy) * t;
|
||||
return;
|
||||
}
|
||||
if (e.mov_after_func == FUNC_MOVE) {
|
||||
// FUNC_MOVE: keep walking past Dst until the next state command (or
|
||||
// the overshoot guard above) — this is what keeps a moving remote PC
|
||||
// from stop-starting between its periodic move packets.
|
||||
e.x = next_x;
|
||||
e.y = next_y;
|
||||
return;
|
||||
}
|
||||
finish_walk(e, true);
|
||||
return;
|
||||
}
|
||||
e.x = next_x;
|
||||
e.y = next_y;
|
||||
}
|
||||
|
||||
// Fallback when the actor's motion speed is unknown: lerp Src->Dst over the
|
||||
// server-supplied dwDuration and stop on time.
|
||||
void EntityStore::advance_walk_by_duration(Entity &e) {
|
||||
float t = e.move_dur_ms == 0
|
||||
? 1.0f
|
||||
: (float)(m_now - e.move_start_ms) / (float)e.move_dur_ms;
|
||||
if (t <= 0.0f) {
|
||||
t = 0.0f;
|
||||
}
|
||||
if (t >= 1.0f) {
|
||||
finish_walk(e, true);
|
||||
} else {
|
||||
e.x = e.sx + (e.tx - e.sx) * t;
|
||||
e.y = e.sy + (e.ty - e.sy) * t;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,20 @@ struct StateCmd {
|
||||
// CLIENT-GAP-FIX.md §3.2.
|
||||
static constexpr uint32_t STATE_QUEUE_MAX_WAIT_MS = 1000;
|
||||
|
||||
// GC_WALK_MODE / TPacketGCWalkMode::mode (40250 Packet.h:2327, server packet.h:1946).
|
||||
enum : uint8_t {
|
||||
WALKMODE_RUN = 0,
|
||||
WALKMODE_WALK = 1,
|
||||
};
|
||||
|
||||
// Reference CInstanceBase::MovementProcess (non-main branch): a remote actor that
|
||||
// has walked more than this far past its Dst re-sources the walk at its current
|
||||
// position, turns back toward Dst and downgrades a pending FUNC_MOVE to FUNC_WAIT.
|
||||
static constexpr float MOVE_OVERSHOOT_CM = 100.0f;
|
||||
// Cap on one tick's walk step so a hitch (or the first tick after a long pause)
|
||||
// cannot teleport an actor far past its Dst in a single frame.
|
||||
static constexpr uint32_t MOVE_MAX_TICK_MS = 250;
|
||||
|
||||
struct Entity {
|
||||
uint32_t vid = 0;
|
||||
uint16_t race = 0;
|
||||
@@ -65,7 +79,7 @@ struct Entity {
|
||||
|
||||
uint8_t func = FUNC_WAIT;
|
||||
uint8_t position = 0;
|
||||
uint8_t walk_mode = 0; // 0 walk, 1 run (server's WALKMODE_*)
|
||||
uint8_t walk_mode = WALKMODE_RUN; // server WALKMODE_* (0 run, 1 walk)
|
||||
uint32_t fly_target_vid = 0;
|
||||
int32_t fly_target_x = 0, fly_target_y = 0;
|
||||
bool fly_target_set = false;
|
||||
@@ -74,6 +88,14 @@ struct Entity {
|
||||
float sx = 0, sy = 0, tx = 0, ty = 0;
|
||||
uint32_t move_start_ms = 0;
|
||||
uint32_t move_dur_ms = 0;
|
||||
// Root-motion speed of the race's WALK / RUN loop at movSpd 100, in cm/s
|
||||
// (.msa Accumulation length / MotionDuration). Pushed by the presentation
|
||||
// layer once the model is built (set_motion_speed). While known, a remote
|
||||
// walk advances like the reference CActorInstance::AccumulationMovement
|
||||
// (motion speed * movSpd/100) and arrives by crossing Dst; while 0 (no model /
|
||||
// headless) it falls back to lerping over the server dwDuration.
|
||||
float walk_motion_speed = 0;
|
||||
float run_motion_speed = 0;
|
||||
|
||||
// CLIENT-GAP §3.2: TCP state queue + the "do this once the walk reaches Dst"
|
||||
// latch (reference m_kMovAfterFunc / m_dwMovAfterArg). `skip_collision` mirrors
|
||||
@@ -516,6 +538,9 @@ public:
|
||||
// and again right after mut_move() enqueues, so an already-due command applies
|
||||
// synchronously.
|
||||
void process_states();
|
||||
// Root-motion speeds (cm/s at movSpd 100) of this actor's WALK / RUN loops;
|
||||
// 0 = unknown. See Entity::walk_motion_speed. No-op for an unknown VID.
|
||||
void set_motion_speed(uint32_t vid, float walk_cm_s, float run_cm_s);
|
||||
|
||||
// Feed one complete game-phase packet. `body` points at the packet start
|
||||
// (header/length included); `len` == that length. Unknown headers ignored.
|
||||
@@ -1176,11 +1201,18 @@ private:
|
||||
void begin_state_walk(Entity &e, const StateCmd &c, uint8_t after_func); // Src/Dst + skip-collision
|
||||
static bool can_process_network_state(const Entity &e); // ~ __CanProcessNetworkStatePacket
|
||||
static bool is_enable_tcp_process(const Entity &e, uint8_t func); // ~ __IsEnableTCPProcess
|
||||
// Remote-walk advance (reference CInstanceBase::MovementProcess, non-main).
|
||||
static float motion_move_speed(const Entity &e); // cm/s, 0 = use dwDuration lerp
|
||||
static void finish_walk(Entity &e, bool snap_to_dst); // m_kMovAfterFunc on arrival
|
||||
void advance_walk_by_motion(Entity &e, float speed, uint32_t dt_ms);
|
||||
void advance_walk_by_duration(Entity &e);
|
||||
|
||||
std::unordered_map<uint32_t, Entity> m_ents;
|
||||
uint32_t m_main_vid = 0;
|
||||
uint32_t m_now = 0;
|
||||
uint32_t m_server_frame_ms = 0;
|
||||
uint32_t m_last_tick_ms = 0; // m_now at the previous tick() (motion-driven walk dt)
|
||||
bool m_ticked = false;
|
||||
std::vector<Change> m_changes;
|
||||
std::vector<uint32_t> m_dirty;
|
||||
std::string m_bgm_name;
|
||||
|
||||
@@ -100,6 +100,8 @@ void M2Client::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("quest_cancel"), &M2Client::quest_cancel);
|
||||
ClassDB::bind_method(D_METHOD("get_quests"), &M2Client::get_quests);
|
||||
ClassDB::bind_method(D_METHOD("get_entity", "vid"), &M2Client::get_entity);
|
||||
ClassDB::bind_method(D_METHOD("set_entity_motion_speed", "vid", "walk_cm_s", "run_cm_s"),
|
||||
&M2Client::set_entity_motion_speed);
|
||||
ClassDB::bind_method(D_METHOD("get_entities"), &M2Client::get_entities);
|
||||
ClassDB::bind_method(D_METHOD("get_main_vid"), &M2Client::get_main_vid);
|
||||
ClassDB::bind_method(D_METHOD("get_main_pid"), &M2Client::get_main_pid);
|
||||
@@ -2523,6 +2525,13 @@ const mtnet::EntityStore *M2Client::active_world() const {
|
||||
return game ? &game->world() : nullptr;
|
||||
}
|
||||
|
||||
void M2Client::set_entity_motion_speed(int vid, double walk_cm_s, double run_cm_s) {
|
||||
mtnet::EntityStore *w = classic_sess ? &classic_sess->world() : (game ? &game->world() : nullptr);
|
||||
if (w) {
|
||||
w->set_motion_speed((uint32_t)vid, (float)walk_cm_s, (float)run_cm_s);
|
||||
}
|
||||
}
|
||||
|
||||
Dictionary M2Client::get_entity(int vid) const {
|
||||
const mtnet::EntityStore *w = active_world();
|
||||
if (!w) {
|
||||
|
||||
@@ -288,6 +288,9 @@ public:
|
||||
|
||||
// --- networked world snapshot (positions already Godot-space, metres) ---
|
||||
godot::Dictionary get_entity(int vid) const;
|
||||
// Root-motion speeds (cm/s at movSpd 100) of a remote actor's WALK / RUN loop,
|
||||
// measured by the view from its .msa; drives the reference-style remote walk.
|
||||
void set_entity_motion_speed(int vid, double walk_cm_s, double run_cm_s);
|
||||
godot::Array get_entities() const;
|
||||
int get_main_vid() const;
|
||||
int get_main_pid() const;
|
||||
|
||||
Reference in New Issue
Block a user