From 69edba036e79f7632d67eebef6a1a99e8f768f62 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Fri, 21 Mar 2025 22:22:43 -0700 Subject: [PATCH] add $whatobj command --- README.md | 9 +- src/ChatCommands.cc | 59 + src/CommonFileFormats.hh | 35 + src/Lobby.cc | 4 + src/Map.cc | 30 + src/Map.hh | 20 +- src/ReceiveCommands.cc | 2 +- src/ServerState.cc | 23 +- src/ServerState.hh | 1 + system/maps/gc-ep3/map_city_on_battle_e.dat | Bin 0 -> 1152 bytes system/maps/gc-ep3/map_city_on_battle_o.dat | Bin 0 -> 1972 bytes system/maps/room-layout-index.json | 3908 +++++++++++++++++++ 12 files changed, 4083 insertions(+), 8 deletions(-) create mode 100755 system/maps/gc-ep3/map_city_on_battle_e.dat create mode 100755 system/maps/gc-ep3/map_city_on_battle_o.dat create mode 100644 system/maps/room-layout-index.json diff --git a/README.md b/README.md index f4b49165..4cb1555a 100644 --- a/README.md +++ b/README.md @@ -554,15 +554,16 @@ Some commands only work on the game server and not on the proxy server. The chat * You'll see the rare seed value and floor variations when you join a game. * You'll be placed into the last available slot in lobbies and games instead of the first, unless you're joining a BB solo-mode game. * You'll be able to join games with any PSO version, not only those for which cross-version play is normally enabled. See the "Cross-version play" section above for details on this. - * The rest of the commands in this section are enabled on the game server. (They are always enabled on the proxy server.) + * Most of the commands in this section are enabled. (A few of them are always enabled and don't require `$debug`.) + * `$whatobj` (game server only): Tells you what the closest object is to your position, along with its coordinates and object ID. The object's full definition is also printed to the server's log. This command can be used without `$debug` enabled. * `$readmem
` (game server only): Read 4 bytes from the given address and show you the values. * `$writemem
` (game server only): Write data to the given address. Data is not required to be any specific size. * `$nativecall
[arg1 ...]` (game server only, GC only): Call a native function on your client. Only arguments passed in registers are supported; calling functions that take many arguments is not supported. - * `$quest ` (game server only): Load a quest by quest number. Can be used to load battle or challenge quests with only one player present. Debug is not required to be enabled if the specified quest has the AllowStartFromChatCommand field set in its metadata file. + * `$quest ` (game server only): Load a quest by quest number. Can be used to load battle or challenge quests with only one player present. `$debug` is not required for this command if the specified quest has the AllowStartFromChatCommand field set in its metadata file. * `$qcall `: Call a quest function on your client. - * `$qcheck ` (game server only): Show the value of a quest flag. This command can be used without debug mode enabled. If you're in a game, show the value of the flag in that game; if you're in the lobby, show the saved value of that quest flag for your character (BB only). + * `$qcheck ` (game server only): Show the value of a quest flag. This command can be used without `$debug` enabled. If you're in a game, show the value of the flag in that game; if you're in the lobby, show the saved value of that quest flag for your character (BB only). * `$qset ` or `$qclear `: Set or clear a quest flag for everyone in the game. If you're in the lobby and on BB, set or clear the saved value of a quest flag in your character file. - * `$qgread ` (game server only): Show the value of a quest counter ("global flag"). This command can be used without debug mode enabled. + * `$qgread ` (game server only): Show the value of a quest counter ("global flag"). This command can be used without `$debug` enabled. * `$qgwrite ` (game server only): Set the value of a quest counter ("global flag") for yourself. * `$qsync `: Set a quest register's value for yourself only. `` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `` is parsed as a floating-point value instead of as an integer. * `$qsyncall `: Set a quest register's value for everyone in the game. `` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `` is parsed as a floating-point value instead of as an integer. diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 0dac0b92..da82a879 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -2771,6 +2771,65 @@ ChatCommandDefinition cc_what( }, unavailable_on_proxy_server); +ChatCommandDefinition cc_whatobj( + {"$whatobj"}, + +[](const ServerArgs& a) -> void { + auto s = a.c->require_server_state(); + auto l = a.c->require_lobby(); + a.check_is_game(true); + + if (!l->map_state) { + throw precondition_failed("$C4No map loaded"); + } + + // TODO: We should use the actual area if a loaded quest has reassigned + // them; it's likely that the variations will be wrong if we don't + auto sdt = s->set_data_table(a.c->version(), l->episode, l->mode, l->difficulty); + uint8_t area = sdt->default_area_for_floor(l->episode, a.c->floor); + uint8_t layout_var = (a.c->floor < 0x10) ? l->variations.entries[a.c->floor].layout.load() : 0x00; + + float min_dist2 = 0.0f; + VectorXYZF nearest_worldspace_pos; + shared_ptr nearest_obj; + for (const auto& it : l->map_state->iter_object_states(a.c->version())) { + if (!it->super_obj || (it->super_obj->floor != a.c->floor)) { + continue; + } + const auto& def = it->super_obj->version(a.c->version()); + if (!def.set_entry) { + continue; + } + VectorXYZF worldspace_pos; + try { + const auto& room = s->room_layout_index->get_room(area, layout_var, def.set_entry->room); + // This is the order in which the game does the rotations; not sure why + worldspace_pos = def.set_entry->pos.rotate_x(room.angle.x).rotate_z(room.angle.z).rotate_y(room.angle.y) + room.position; + } catch (const out_of_range&) { + a.c->log.warning("Can't find definition for room %02hhX:%02hhX:%08hX", area, layout_var, def.set_entry->room.load()); + worldspace_pos = def.set_entry->pos; + } + float dist2 = (VectorXZF(worldspace_pos) - a.c->pos).norm2(); + if (!nearest_obj || (dist2 < min_dist2)) { + nearest_obj = it; + nearest_worldspace_pos = worldspace_pos; + min_dist2 = dist2; + } + } + + if (!nearest_obj) { + throw precondition_failed("$C4No objects nearby"); + } else { + send_text_message_printf(a.c, "$C5K-%03zX\n$C6%s\nX:%.2f Z:%.2f", + nearest_obj->k_id, nearest_obj->type_name(a.c->version()), + nearest_worldspace_pos.x.load(), nearest_worldspace_pos.z.load()); + auto set_str = nearest_obj->super_obj->version(a.c->version()).set_entry->str(); + a.c->log.info("Object found via $whatobj: K-%03zX %s at x=%g y=%g z=%g", + nearest_obj->k_id, set_str.c_str(), + nearest_worldspace_pos.x.load(), nearest_worldspace_pos.y.load(), nearest_worldspace_pos.z.load()); + } + }, + unavailable_on_proxy_server); + ChatCommandDefinition cc_where( {"$where"}, +[](const ServerArgs& a) -> void { diff --git a/src/CommonFileFormats.hh b/src/CommonFileFormats.hh index 18c71b14..bec38ff0 100644 --- a/src/CommonFileFormats.hh +++ b/src/CommonFileFormats.hh @@ -5,6 +5,10 @@ #include "Text.hh" +constexpr double radians_for_fixed_point_angle(uint16_t angle) { + return static_cast(angle * 2 * M_PI) / 0x10000; +} + struct VectorXZF { le_float x = 0.0; le_float z = 0.0; @@ -34,6 +38,12 @@ struct VectorXZF { return ((this->x * this->x) + (this->z * this->z)); } + inline VectorXZF rotate_y(double angle) const { + double s = sin(angle); + double c = cos(angle); + return VectorXZF(this->x * c - this->z * s, this->x * s + this->z * c); + } + inline std::string str() const { return phosg::string_printf("[VectorXZF x=%g z=%g]", this->x.load(), this->z.load()); } @@ -73,6 +83,31 @@ struct VectorXYZF { return ((this->x * this->x) + (this->y * this->y) + (this->z * this->z)); } + inline VectorXYZF rotate_x(double angle) const { + double s = sin(angle); + double c = cos(angle); + return VectorXYZF( + this->x, + this->y * c - this->z * s, + this->y * s + this->z * c); + } + inline VectorXYZF rotate_y(double angle) const { + double s = sin(angle); + double c = cos(angle); + return VectorXYZF( + this->x * c + this->z * s, + this->y, + -this->x * s + this->z * c); + } + inline VectorXYZF rotate_z(double angle) const { + double s = sin(angle); + double c = cos(angle); + return VectorXYZF( + this->x * c - this->y * s, + this->x * s + this->y * c, + this->z); + } + inline std::string str() const { return phosg::string_printf("[VectorXYZF x=%g y=%g z=%g]", this->x.load(), this->y.load(), this->z.load()); } diff --git a/src/Lobby.cc b/src/Lobby.cc index 303128a0..d90b2c6e 100644 --- a/src/Lobby.cc +++ b/src/Lobby.cc @@ -315,6 +315,10 @@ void Lobby::load_maps() { this->opt_rand_crypt, s->supermaps_for_variations(this->episode, this->mode, this->difficulty, this->variations)); } + + if (this->check_flag(Lobby::Flag::DEBUG)) { + this->map_state->print(stderr); + } } [[nodiscard]] bool Lobby::is_ep3_nte() const { diff --git a/src/Map.cc b/src/Map.cc index 7278ed17..1d9eb218 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -5418,3 +5418,33 @@ void MapState::print(FILE* stream) const { fprintf(stream, " %s set_flags=%04hX\n", ev_str.c_str(), ev_st->flags); } } + +static constexpr uint64_t room_layout_key(uint8_t area, uint8_t major_var, uint32_t room_id) { + return ((static_cast(area) << 40) | + (static_cast(major_var) << 32) | + static_cast(room_id)); +} + +RoomLayoutIndex::RoomLayoutIndex(const phosg::JSON& json) { + for (const auto& [area_and_major_var, rooms_json] : json.as_dict()) { + auto tokens = phosg::split(area_and_major_var, '-'); + uint8_t area = stoul(tokens.at(0), nullptr, 16); + uint8_t major_var = stoul(tokens.at(1), nullptr, 16); + for (const auto& [room_id_str, room_json] : rooms_json->as_dict()) { + uint32_t room_id = stoul(room_id_str, nullptr, 16); + auto emplace_ret = this->rooms.emplace(room_layout_key(area, major_var, room_id), Room()); + auto& room = emplace_ret.first->second; + const auto& l = room_json->as_list(); + room.position.x = l.at(0)->as_float(); + room.position.y = l.at(1)->as_float(); + room.position.z = l.at(2)->as_float(); + room.angle.x = l.at(3)->as_int(); + room.angle.y = l.at(4)->as_int(); + room.angle.z = l.at(5)->as_int(); + } + } +} + +const RoomLayoutIndex::Room& RoomLayoutIndex::get_room(uint8_t area, uint8_t major_var, uint32_t room_id) const { + return this->rooms.at(room_layout_key(area, major_var, room_id)); +} diff --git a/src/Map.hh b/src/Map.hh index f6bf7fb2..ff07c7e6 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -173,7 +173,14 @@ public: /* 0A */ le_uint16_t group = 0; /* 0C */ le_uint16_t room = 0; /* 0E */ le_uint16_t unknown_a3 = 0; + // The position is relative to the room in which the object is placed; to + // get the actual world position, the object's position must be rotated + // around the room's origin by the room's angles, then translated by the + // room's offset. The room's angle and offset can be found in the area's + // n.rel file. /* 10 */ VectorXYZF pos; + // Angles are specified as 16-bit integers, where 0 is no rotation around + // the axis and FFFF is almost a complete counterclockwise rotation. /* 1C */ VectorXYZI angle; /* 28 */ le_float fparam1 = 0.0f; // Boxes: if <= 0, this is a specialized box, and the specialization is in param4/5/6 /* 2C */ le_float fparam2 = 0.0f; @@ -697,7 +704,7 @@ public: inline const char* type_name(Version v) const { return this->super_obj ? MapFile::name_for_object_type(this->super_obj->version(v).set_entry->base_type) - : ""; + : ""; } }; @@ -984,3 +991,14 @@ public: void verify() const; void print(FILE* stream) const; }; + +struct RoomLayoutIndex { + struct Room { + VectorXYZF position; + VectorXYZI angle; + }; + std::unordered_map rooms; + + explicit RoomLayoutIndex(const phosg::JSON& json); + const Room& get_room(uint8_t area, uint8_t major_var, uint32_t room_id) const; +}; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index f6e43a67..8075e71a 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -4391,7 +4391,7 @@ shared_ptr create_game_generic( break; } - if (creator_c->login->account->check_flag(Account::Flag::DEBUG)) { + if (creator_c->config.check_flag(Client::Flag::DEBUG_ENABLED)) { game->set_flag(Lobby::Flag::DEBUG); } if (creator_c->config.check_flag(Client::Flag::IS_CLIENT_CUSTOMIZATION)) { diff --git a/src/ServerState.cc b/src/ServerState.cc index eca3513c..80f57ab9 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -1568,10 +1568,27 @@ void ServerState::load_patch_indexes(bool from_non_event_thread) { void ServerState::load_maps(bool from_non_event_thread) { using SDT = SetDataTable; - config_log.info("Loading free play map files"); + config_log.info("Loading map layouts"); + auto room_layout_index = make_shared( + phosg::JSON::parse(phosg::load_file("system/maps/room-layout-index.json"))); + config_log.info("Loading Episode 3 Morgue maps"); unordered_map> new_map_file_for_source_hash; map, NUM_VERSIONS>> new_map_files_for_free_play_key; + { + // TODO: Ep3 NTE loads map_city00_on, but it appears there are some + // variants. Figure this out and load those maps too. + auto objects_data = this->load_map_file(Version::GC_EP3, "system/maps/gc-ep3/map_city_on_battle_o.dat"); + auto enemies_data = this->load_map_file(Version::GC_EP3, "system/maps/gc-ep3/map_city_on_battle_e.dat"); + if (objects_data || enemies_data) { + uint32_t free_play_key = this->free_play_key(Episode::EP3, GameMode::NORMAL, 0, 0, 0, 0); + auto map_file = make_shared(0, objects_data, enemies_data, nullptr); + new_map_file_for_source_hash.emplace(map_file->source_hash(), map_file); + new_map_files_for_free_play_key[free_play_key].at(static_cast(Version::GC_EP3)) = map_file; + } + } + + config_log.info("Loading free play map files"); for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) { const array episodes = {Episode::EP1, Episode::EP2, Episode::EP4}; for (Episode episode : episodes) { @@ -1656,9 +1673,11 @@ void ServerState::load_maps(bool from_non_event_thread) { auto set = [s = this->shared_from_this(), new_map_file_for_source_hash = std::move(new_map_file_for_source_hash), - new_map_files_for_free_play_key = std::move(new_map_files_for_free_play_key)]() { + new_map_files_for_free_play_key = std::move(new_map_files_for_free_play_key), + new_room_layout_index = std::move(room_layout_index)]() { s->map_file_for_source_hash = std::move(new_map_file_for_source_hash); s->map_files_for_free_play_key = std::move(new_map_files_for_free_play_key); + s->room_layout_index = new_room_layout_index; s->supermap_for_source_hash_sum.clear(); s->supermap_for_free_play_key.clear(); }; diff --git a/src/ServerState.hh b/src/ServerState.hh index 7bddf9d4..99ad4ca5 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -177,6 +177,7 @@ struct ServerState : public std::enable_shared_from_this { std::map, NUM_VERSIONS>> map_files_for_free_play_key; std::unordered_map> supermap_for_source_hash_sum; std::unordered_map> supermap_for_free_play_key; + std::shared_ptr room_layout_index; std::shared_ptr bb_stream_files_cache; std::shared_ptr bb_system_cache; std::shared_ptr gba_files_cache; diff --git a/system/maps/gc-ep3/map_city_on_battle_e.dat b/system/maps/gc-ep3/map_city_on_battle_e.dat new file mode 100755 index 0000000000000000000000000000000000000000..b8add26f981f9cff9f01a18268f43cb081375482 GIT binary patch literal 1152 zcmZ{jF;7%M5XXmi8c-=FaBz3P!Q&(pSX@FZw17MgA=*(;5Tu|YF%%LSDwA#ONZ<>Y zyicGFh2?p#x#H}^%7Wg8#^SCp&VOf@U5HF_vo|}lzxSVc`yP|n7#xoeVX=0werwG& zB1+f%Ke`&(AX>F`rvHb2ATbWdWzYHEdt>uwoyDSr)H&Q8Ym;z}TG%!HzWGU%P#Fuk!47s>|37agm-BoY#)?m&R~-0(CqYI3qSsw5=;656gw^EGr48+rk+$NQ~`k z5;?8}PPOB!?YiQkSPvz_$yLu_m@ZLLf#Yi6q)rd^#oE@3i~7pJxp15(#&W8t<67YC z7JBl7t~Y%}oc$r;oH`CuiaFOTXDVzw6fs70JB%;%TmlyyE+mNOkVhfdGo zdI_n0I9SA?Vd13qIc8AP_ce_go(UYhC(&a*7YS0=aIlDz;T}GBoVx0nK@Haf=eWQQ zQM0gndm7#H=7DgoU0)5{=Y;C0;91Y<^|sfb=i~49sweXq(RgFcO?i&|aDNt!+{1)s z5%HX_7PRO=U!U~5$dJF^%-|XH5Y54H>N#&uzI3odI}5E6)(j37>B%wYB{!d@d4%*l z95+0NXzQDvInut`$ipHIbzjMMAwTr>!Y~du5b@K%*%LiD^HS&izkAqzD0)Vno)!8B D&y080 literal 0 HcmV?d00001 diff --git a/system/maps/gc-ep3/map_city_on_battle_o.dat b/system/maps/gc-ep3/map_city_on_battle_o.dat new file mode 100755 index 0000000000000000000000000000000000000000..8acab83f07ae64e9b6fb4ad8bc3fd52add2c8585 GIT binary patch literal 1972 zcmb8wPe>F|7y$6kCHkWn;UOq9HtnBvvHVlHnr~dw4pEz_C2g=l2W1aRT_P_7Q3o%1 z>fjPANYNzbYK6+yN-c#Db^;a4zCZJ3+ttB(u=8f$``&N9_wCH=mxpLLJI+($BQT8(i&!+NTq7`avz8^5Q5wz0TqM?3des?lWf{1!BDCCycK*0S-qwKmOA zYWX})d)dHRs1`9imxE|T%lbyE#2DfTIw%+8DNVv247e!9>sSNl@!>{8S{@Xre_uM6 zW?E-Ld{%%)vlt6M3^sDYXVVP5p88}nEnG-#HyU9vp8L+RQ9rB1cp)KpRLkel9OZ~y z48RDgY2EoFY`ptila@!aro6^Jn6p+FE(O?xMv)kSk;vfi!#tyRNZ)IAFy}$JfLiMe zVPxhQP_FgAS3J1Jy#<$)rhlEfJTWJ*TRW`SdV2CU%KULWThCBU6URh z*oa1{7y~~`jQYNpM*FW;KZm|w`n>!_U_HJmHmDwDVgS@u%X_R`jH`M@vZi{3dUNjk zhH;(80d0h^rgyO>7sSZdE8L8!5F48@kAvEn!S(I<=TkI_4T_@2RqMf+A}v_ZYqkQcaTqv#NH?UrHO