diff --git a/README.md b/README.md index 5bf4a908..cd6b8f18 100644 --- a/README.md +++ b/README.md @@ -555,7 +555,7 @@ Some commands only work on the game server and not on the proxy server. The chat * 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. * 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. + * `$whatobj` and `$whatene` (game server only): Tells you what the closest object or enemy spawn point is to your position, along with its coordinates and object or enemy ID. The full definition is also printed to the server's log. These commands 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. diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index f9345068..cba1b67c 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -2771,100 +2771,114 @@ ChatCommandDefinition cc_what( }, unavailable_on_proxy_server); +static void whatobj_whatene_fn(const ServerArgs& a, bool include_objs, bool include_enes) { + 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 + uint8_t area, layout_var; + if (l->episode != Episode::EP3) { + auto sdt = s->set_data_table(a.c->version(), l->episode, l->mode, l->difficulty); + area = sdt->default_area_for_floor(l->episode, a.c->floor); + layout_var = (a.c->floor < 0x10) ? l->variations.entries[a.c->floor].layout.load() : 0x00; + } else { + area = a.c->floor; + layout_var = 0; + } + + double min_dist2 = -1.0; + VectorXYZF nearest_worldspace_pos; + shared_ptr nearest_obj; + shared_ptr nearest_ene; + + auto check_entity = [&](auto& nearest_entity, auto entity, const auto& def) -> void { + VectorXYZF worldspace_pos; + if (l->episode != Episode::EP3) { + 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; + } + } else { + worldspace_pos = def.set_entry->pos; + } + + float dist2 = (VectorXZF(worldspace_pos) - a.c->pos).norm2(); + if ((min_dist2 < 0.0) || (dist2 < min_dist2)) { + nearest_entity = entity; + nearest_worldspace_pos = worldspace_pos; + min_dist2 = dist2; + } + }; + + if (include_objs) { + for (const auto& it : l->map_state->iter_object_states(a.c->version())) { + if (it->super_obj && (it->super_obj->floor == a.c->floor)) { + const auto& def = it->super_obj->version(a.c->version()); + if (def.set_entry) { + check_entity(nearest_obj, it, def); + } + } + } + } + if (include_enes) { + for (const auto& it : l->map_state->iter_enemy_states(a.c->version())) { + if (it->super_ene && (it->super_ene->floor == a.c->floor)) { + const auto& def = it->super_ene->version(a.c->version()); + if (def.set_entry) { + check_entity(nearest_ene, it, def); + } + } + } + } + + // Since we check all objects first, nearest_ene will only be set if + // there is an enemy closer than all objects. So, we print that if it's + // set, and print the object if not. + if (nearest_ene) { + const auto* set_entry = nearest_ene->super_ene->version(a.c->version()).set_entry; + send_text_message_printf(a.c, "$C5E-%03zX\n$C6%s\n$C2%s\n$C7X:%.2f Z:%.2f", + nearest_ene->e_id, phosg::name_for_enum(nearest_ene->type(a.c->version(), l->episode, l->event)), + MapFile::name_for_enemy_type(set_entry->base_type), + nearest_worldspace_pos.x.load(), nearest_worldspace_pos.z.load()); + auto set_str = set_entry->str(); + a.c->log.info("Enemy found via $whatobj: E-%03zX %s at x=%g y=%g z=%g", + nearest_ene->e_id, set_str.c_str(), + nearest_worldspace_pos.x.load(), nearest_worldspace_pos.y.load(), nearest_worldspace_pos.z.load()); + + } else if (nearest_obj) { + send_text_message_printf(a.c, "$C5K-%03zX\n$C6%s\n$C7X:%.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()); + + } else { + throw precondition_failed("$C4No objects or\nenemies are nearby"); + } +} + 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 - uint8_t area, layout_var; - if (l->episode != Episode::EP3) { - auto sdt = s->set_data_table(a.c->version(), l->episode, l->mode, l->difficulty); - area = sdt->default_area_for_floor(l->episode, a.c->floor); - layout_var = (a.c->floor < 0x10) ? l->variations.entries[a.c->floor].layout.load() : 0x00; - } else { - area = a.c->floor; - layout_var = 0; - } - - double min_dist2 = -1.0; - VectorXYZF nearest_worldspace_pos; - shared_ptr nearest_obj; - shared_ptr nearest_ene; - - auto check_entity = [&](auto& nearest_entity, auto entity, const auto& def) -> void { - VectorXYZF worldspace_pos; - if (l->episode != Episode::EP3) { - 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; - } - } else { - worldspace_pos = def.set_entry->pos; - } - - float dist2 = (VectorXZF(worldspace_pos) - a.c->pos).norm2(); - if ((min_dist2 < 0.0) || (dist2 < min_dist2)) { - nearest_entity = entity; - nearest_worldspace_pos = worldspace_pos; - min_dist2 = dist2; - } - }; - - for (const auto& it : l->map_state->iter_object_states(a.c->version())) { - if (it->super_obj && (it->super_obj->floor == a.c->floor)) { - const auto& def = it->super_obj->version(a.c->version()); - if (def.set_entry) { - check_entity(nearest_obj, it, def); - } - } - } - for (const auto& it : l->map_state->iter_enemy_states(a.c->version())) { - if (it->super_ene && (it->super_ene->floor == a.c->floor)) { - const auto& def = it->super_ene->version(a.c->version()); - if (def.set_entry) { - check_entity(nearest_ene, it, def); - } - } - } - - // Since we check all objects first, nearest_ene will only be set if - // there is an enemy closer than all objects. So, we print that if it's - // set, and print the object if not. - if (nearest_ene) { - const auto* set_entry = nearest_ene->super_ene->version(a.c->version()).set_entry; - send_text_message_printf(a.c, "$C5E-%03zX\n$C6%s\n$C2%s\n$C7X:%.2f Z:%.2f", - nearest_ene->e_id, phosg::name_for_enum(nearest_ene->type(a.c->version(), l->episode, l->event)), - MapFile::name_for_enemy_type(set_entry->base_type), - nearest_worldspace_pos.x.load(), nearest_worldspace_pos.z.load()); - auto set_str = set_entry->str(); - a.c->log.info("Enemy found via $whatobj: E-%03zX %s at x=%g y=%g z=%g", - nearest_ene->e_id, set_str.c_str(), - nearest_worldspace_pos.x.load(), nearest_worldspace_pos.y.load(), nearest_worldspace_pos.z.load()); - - } else if (nearest_obj) { - send_text_message_printf(a.c, "$C5K-%03zX\n$C6%s\n$C7X:%.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()); - - } else { - throw precondition_failed("$C4No objects or\nenemies are nearby"); - } + whatobj_whatene_fn(a, true, false); + }, + unavailable_on_proxy_server); +ChatCommandDefinition cc_whatene( + {"$whatene"}, + +[](const ServerArgs& a) -> void { + whatobj_whatene_fn(a, false, true); }, unavailable_on_proxy_server);