add $whatobj command

This commit is contained in:
Martin Michelsen
2025-03-21 22:22:43 -07:00
parent ca1dc6ad7d
commit 69edba036e
12 changed files with 4083 additions and 8 deletions
+5 -4
View File
@@ -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 <address>` (game server only): Read 4 bytes from the given address and show you the values.
* `$writemem <address> <data>` (game server only): Write data to the given address. Data is not required to be any specific size.
* `$nativecall <address> [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 <number>` (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 <number>` (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 <function-id>`: Call a quest function on your client.
* `$qcheck <flag-num>` (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 <flag-num>` (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 <flag-num>` or `$qclear <flag-num>`: 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 <flag-num>` (game server only): Show the value of a quest counter ("global flag"). This command can be used without debug mode enabled.
* `$qgread <flag-num>` (game server only): Show the value of a quest counter ("global flag"). This command can be used without `$debug` enabled.
* `$qgwrite <flag-num> <value>` (game server only): Set the value of a quest counter ("global flag") for yourself.
* `$qsync <reg-num> <value>`: Set a quest register's value for yourself only. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
* `$qsyncall <reg-num> <value>`: Set a quest register's value for everyone in the game. `<reg-num>` should be either rXX (e.g. r60) or fXX (e.g. f60); if the latter, `<value>` is parsed as a floating-point value instead of as an integer.
+59
View File
@@ -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<const MapState::ObjectState> 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 {
+35
View File
@@ -5,6 +5,10 @@
#include "Text.hh"
constexpr double radians_for_fixed_point_angle(uint16_t angle) {
return static_cast<double>(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());
}
+4
View File
@@ -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 {
+30
View File
@@ -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<uint64_t>(area) << 40) |
(static_cast<uint64_t>(major_var) << 32) |
static_cast<uint64_t>(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));
}
+19 -1
View File
@@ -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)
: "<PLAYER TRAP>";
: "<DYNAMIC>";
}
};
@@ -984,3 +991,14 @@ public:
void verify() const;
void print(FILE* stream) const;
};
struct RoomLayoutIndex {
struct Room {
VectorXYZF position;
VectorXYZI angle;
};
std::unordered_map<uint64_t, Room> rooms;
explicit RoomLayoutIndex(const phosg::JSON& json);
const Room& get_room(uint8_t area, uint8_t major_var, uint32_t room_id) const;
};
+1 -1
View File
@@ -4391,7 +4391,7 @@ shared_ptr<Lobby> 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)) {
+21 -2
View File
@@ -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<RoomLayoutIndex>(
phosg::JSON::parse(phosg::load_file("system/maps/room-layout-index.json")));
config_log.info("Loading Episode 3 Morgue maps");
unordered_map<uint64_t, shared_ptr<const MapFile>> new_map_file_for_source_hash;
map<uint32_t, array<shared_ptr<const MapFile>, 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<MapFile>(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<size_t>(Version::GC_EP3)) = map_file;
}
}
config_log.info("Loading free play map files");
for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) {
const array<Episode, 3> 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();
};
+1
View File
@@ -177,6 +177,7 @@ struct ServerState : public std::enable_shared_from_this<ServerState> {
std::map<uint32_t, std::array<std::shared_ptr<const MapFile>, NUM_VERSIONS>> map_files_for_free_play_key;
std::unordered_map<uint64_t, std::shared_ptr<const SuperMap>> supermap_for_source_hash_sum;
std::unordered_map<uint32_t, std::shared_ptr<const SuperMap>> supermap_for_free_play_key;
std::shared_ptr<const RoomLayoutIndex> room_layout_index;
std::shared_ptr<FileContentsCache> bb_stream_files_cache;
std::shared_ptr<FileContentsCache> bb_system_cache;
std::shared_ptr<FileContentsCache> gba_files_cache;
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff