From 9d59bc9222930fa45d9a24898533dec5b178b700 Mon Sep 17 00:00:00 2001 From: geneotech Date: Sun, 26 Nov 2023 18:47:27 +0100 Subject: [PATCH] Give items to players from RCON. Warp them too. Make them rich, also other commands like round_restart. --- docs/pages/todo/brainstorm_now.md | 56 +++--- docs/pages/todo/todo_done.md | 3 + hypersomnia/default_config.lua | 1 + src/application/app_intent_type.h | 1 + .../gui/client/client_gui_state.cpp | 5 + src/application/gui/client/rcon_gui.h | 3 + src/application/gui/client/rcon_gui.hpp | 32 ++++ src/application/network/net_serialize.h | 6 + src/application/network/rcon_command.h | 19 +- .../setups/client/client_setup.cpp | 15 +- .../setups/server/server_setup.cpp | 15 +- src/augs/network/network_types.h | 1 + src/game/components/text_details_component.h | 1 + .../detail/inventory/generate_equipment.h | 130 +++++++------ .../detail/inventory/requested_equipment.h | 3 +- src/game/modes/arena_mode.cpp | 174 +++++++++++++++--- src/game/modes/arena_mode.h | 17 +- .../mode_commands/mode_entropy_structs.h | 5 +- .../mode_commands/translate_game_commands.h | 132 +++++++++++++ src/test_scenes/test_scene_flavours.h | 1 + 20 files changed, 489 insertions(+), 131 deletions(-) create mode 100644 src/game/modes/mode_commands/translate_game_commands.h diff --git a/docs/pages/todo/brainstorm_now.md b/docs/pages/todo/brainstorm_now.md index f44ae7143d..dc97f92443 100644 --- a/docs/pages/todo/brainstorm_now.md +++ b/docs/pages/todo/brainstorm_now.md @@ -6,30 +6,36 @@ permalink: brainstorm_now summary: That which we are brainstorming at the moment. --- -- Miniature generation should trigger "dirty" revision - # BEFORE STEAM -- Add Join our Discord link to description -- Trailer - - Gameplay segment - - Decide on which maps to show - we don't have do to a lot - - de_cyberaqua - - virtual_reality_1 - - de_silo - - de_rambo - - de_duel_practice - - For trailer it's enough only the shown maps are copyright-free - - We can fix other maps post-release - - 1) No intro, just fade into gameplay right away - - 2) A ~5 second gameplay segment per each map with map name shown in top left every time - - Editor segment - - 1) "Create custom maps from scratch!" - Simple Map creation timelapse - - Just make a good map offline to prepare a long command history. THEN turn on obs recording whilst you replay actions with Redo. - - (Instead of trying to record a perfect session of you creating the map) - - 2) "Clone existing maps!" ~10 sec segment - Close up on tweaking existing full-blown maps like cyberaqua with shown mouse movements. - - 3) "Playtest your work online with a single click!" - ~5 sec segment +## For trailer + +- simple chat/rcon commands to set players by nickname in position and give them weapons would come a long way + - text area in rcon even + - on exec, autorestart round if it's won/lost already + - give a hotkey to exec custom rcon commands + - seteq nickname 2Xzamiec electric_armor + - we have to do 2X because with "zamiec zamiec" we don't know if the other should be held or in backpack + +- Preps + - Decide sound effects + +## Post trailer (During review) + +- fix those movement flags not propagading through respawns in ffa + +- Host multiple official servers to accomodate a potential spike in the number of players + - Each instance will can have the same config directory + - Except they will have differing configs for map cycles, no? + - We can make a --config CLI to read another config from the cwd that works like config.force.lua + - server1.force.lua + - server2.force.lua (all can be next to config.force.lua) + - etc. + - Otherwise they'll all just read from .config/Hypersomnia or local folder + +- Miniature generation should trigger "dirty" revision +- logarithmic audio slider - we'll seriously need to support reading user files from a different cwd - + trivial appimage launches @@ -40,12 +46,8 @@ summary: That which we are brainstorming at the moment. - set by AppRun to $HOME/.config so we don't have to call readenv - default will just work like always -- Consider taking the small logo from hypersomnia overlayed over pavement rather than glass - - -- logarithmic audio slider - -- fix small capsule +- GIFs for Steam description +- Add Join our Discord link to description # REST diff --git a/docs/pages/todo/todo_done.md b/docs/pages/todo/todo_done.md index 656a4201de..85305f5349 100644 --- a/docs/pages/todo/todo_done.md +++ b/docs/pages/todo/todo_done.md @@ -6642,3 +6642,6 @@ This will discard your redo history." - Ricochets chapter should come immediately after shooting chapter - Leave backpack/deagles akimbo chapter at the end +- Consider taking the small logo from hypersomnia overlayed over pavement rather than glass + +- fix small capsule diff --git a/hypersomnia/default_config.lua b/hypersomnia/default_config.lua index 09b0930d63..9ae62ce3e0 100644 --- a/hypersomnia/default_config.lua +++ b/hypersomnia/default_config.lua @@ -130,6 +130,7 @@ return { U = "TEAM_CHAT", F8 = "SERVER_ADMIN_PANEL", + F10 = "EXECUTE_RCON_GAME_COMMANDS", RightMouseButton = "SPECTATE_PREVIOUS", LeftMouseButton = "SPECTATE_NEXT" diff --git a/src/application/app_intent_type.h b/src/application/app_intent_type.h index b7184976d1..973ac5c926 100644 --- a/src/application/app_intent_type.h +++ b/src/application/app_intent_type.h @@ -26,6 +26,7 @@ enum class general_gui_intent_type { TEAM_CHAT, BUY_MENU, SERVER_ADMIN_PANEL, + EXECUTE_RCON_GAME_COMMANDS, CHOOSE_TEAM, diff --git a/src/application/gui/client/client_gui_state.cpp b/src/application/gui/client/client_gui_state.cpp index 363342f38b..400811f906 100644 --- a/src/application/gui/client/client_gui_state.cpp +++ b/src/application/gui/client/client_gui_state.cpp @@ -20,6 +20,11 @@ bool client_gui_state::control(const handle_input_before_game_input in) { s = !s; return true; } + + if (*it == general_gui_intent_type::EXECUTE_RCON_GAME_COMMANDS) { + rcon.request_execute_custom_game_commands = true; + return true; + } auto invoke_chat = [&](const chat_target_type t) { chat.open_input_bar(t); diff --git a/src/application/gui/client/rcon_gui.h b/src/application/gui/client/rcon_gui.h index d7c9de3b13..aa4478226d 100644 --- a/src/application/gui/client/rcon_gui.h +++ b/src/application/gui/client/rcon_gui.h @@ -2,6 +2,7 @@ #include "application/setups/client/rcon_pane.h" #include "application/setups/server/server_vars.h" #include "application/setups/server/rcon_level.h" +#include "game/modes/mode_commands/mode_entropy_structs.h" #include "augs/log.h" struct rcon_gui_state { @@ -12,6 +13,8 @@ struct rcon_gui_state { server_vars edited_sv_vars; server_runtime_info runtime_info; + custom_game_commands_string_type custom_commands_text; + bool request_execute_custom_game_commands = false; bool show = false; diff --git a/src/application/gui/client/rcon_gui.hpp b/src/application/gui/client/rcon_gui.hpp index d043e63486..b0a3840d0b 100644 --- a/src/application/gui/client/rcon_gui.hpp +++ b/src/application/gui/client/rcon_gui.hpp @@ -5,6 +5,19 @@ #include "augs/misc/imgui/imgui_utils.h" #include "application/setups/debugger/detail/maybe_different_colors.h" +template +void do_pending_rcon_payloads( + rcon_gui_state& state, + F&& on_new_payload +) { + auto& custom_commands = state.custom_commands_text; + + if (state.request_execute_custom_game_commands) { + on_new_payload(custom_commands); + state.request_execute_custom_game_commands = false; + } +} + template void perform_rcon_gui( rcon_gui_state& state, @@ -23,6 +36,7 @@ void perform_rcon_gui( centered_text(window_name); const auto level = state.level; + auto& custom_commands = state.custom_commands_text; if (level == rcon_level_type::INTEGRATED_ONLY) { text_color("This is an integrated server. Only the host has RCON access.", red); @@ -113,6 +127,24 @@ void perform_rcon_gui( do_command_button(format_enum(cmd), cmd); }); + ImGui::Separator(); + + text_color("Execute custom game commands", yellow); + + input_multiline_text("##CommandsArea", custom_commands, 10); + + { + const auto len = custom_commands.length(); + const bool len_valid = len > 0 && len <= default_max_std_string_length_v; + + auto scope = maybe_disabled_cols({}, !len_valid); + + if (ImGui::Button("Execute") || state.request_execute_custom_game_commands) { + on_new_payload(custom_commands); + state.request_execute_custom_game_commands = false; + } + } + break; diff --git a/src/application/network/net_serialize.h b/src/application/network/net_serialize.h index 055a6d630b..83ff4c7855 100644 --- a/src/application/network/net_serialize.h +++ b/src/application/network/net_serialize.h @@ -270,6 +270,12 @@ namespace net_messages { return true; } + template + bool serialize(Stream& s, std::string& e) { + /* Default to max 8 KB per std::string */ + return serialize_stdstring(s, e, 0, default_max_std_string_length_v); + } + template bool serialize(Stream& s, wielding_setup& p) { return serialize_trivial_as_bytes(s, p); diff --git a/src/application/network/rcon_command.h b/src/application/network/rcon_command.h index c87495c5c4..81a9f94944 100644 --- a/src/application/network/rcon_command.h +++ b/src/application/network/rcon_command.h @@ -2,18 +2,21 @@ #include "application/setups/server/server_vars.h" #include "game/modes/mode_commands/match_command.h" +using custom_game_commands_string_type = std::string; + enum class server_maintenance_command : unsigned char { - SHUTDOWN, - RESTART, - CHECK_FOR_UPDATES_NOW, - REQUEST_RUNTIME_INFO, - DOWNLOAD_LOGS, + SHUTDOWN, + RESTART, + CHECK_FOR_UPDATES_NOW, + REQUEST_RUNTIME_INFO, + DOWNLOAD_LOGS, - COUNT - }; + COUNT +}; using rcon_command_variant = std::variant< std::monostate, match_command, - server_maintenance_command + server_maintenance_command, + custom_game_commands_string_type >; diff --git a/src/application/setups/client/client_setup.cpp b/src/application/setups/client/client_setup.cpp index e1cf852885..5206970d48 100644 --- a/src/application/setups/client/client_setup.cpp +++ b/src/application/setups/client/client_setup.cpp @@ -1214,7 +1214,7 @@ custom_imgui_result client_setup::perform_custom_imgui( rcon_gui.show = false; } - if (!arena_gui.scoreboard.show && rcon_gui.show) { + { auto on_new_payload = [&](const P& new_payload) { if constexpr(std::is_same_v) { send_payload( @@ -1233,11 +1233,18 @@ custom_imgui_result client_setup::perform_custom_imgui( } }; - const bool is_remote_server = true; + if (!arena_gui.scoreboard.show && rcon_gui.show) { + const bool is_remote_server = true; - perform_rcon_gui( + ::perform_rcon_gui( + rcon_gui, + is_remote_server, + on_new_payload + ); + } + + ::do_pending_rcon_payloads( rcon_gui, - is_remote_server, on_new_payload ); } diff --git a/src/application/setups/server/server_setup.cpp b/src/application/setups/server/server_setup.cpp index 31121ffd02..e94b0f5dca 100644 --- a/src/application/setups/server/server_setup.cpp +++ b/src/application/setups/server/server_setup.cpp @@ -1864,7 +1864,7 @@ message_handler_result server_setup::handle_rcon_payload( constexpr auto abort_v = message_handler_result::ABORT_AND_DISCONNECT; constexpr auto continue_v = message_handler_result::CONTINUE; - if constexpr(std::is_same_v) { + if constexpr(is_one_of_v) { local_collected.mode_general.special_command = typed_payload; return continue_v; @@ -2312,11 +2312,11 @@ custom_imgui_result server_setup::perform_custom_imgui(const perform_custom_imgu auto& rcon_gui = integrated_client_gui.rcon; - if (!arena_gui.scoreboard.show && rcon_gui.show) { - auto on_new_payload = [&](const auto& new_payload) { - handle_rcon_payload(rcon_level_type::MASTER, new_payload); - }; + auto on_new_payload = [&](const auto& new_payload) { + handle_rcon_payload(rcon_level_type::MASTER, new_payload); + }; + if (!arena_gui.scoreboard.show && rcon_gui.show) { const bool has_maintenance = false; rcon_gui.level = rcon_level_type::MASTER; @@ -2327,6 +2327,11 @@ custom_imgui_result server_setup::perform_custom_imgui(const perform_custom_imgu on_new_payload ); } + + ::do_pending_rcon_payloads( + rcon_gui, + on_new_payload + ); } } diff --git a/src/augs/network/network_types.h b/src/augs/network/network_types.h index 30080f1f15..3f520e0cfb 100644 --- a/src/augs/network/network_types.h +++ b/src/augs/network/network_types.h @@ -34,6 +34,7 @@ constexpr std::size_t block_fragment_size_v = 1 * 1024; constexpr std::size_t max_packet_size_v = 4 * 1024; constexpr std::size_t max_address_string_length_v = 255; +constexpr std::size_t default_max_std_string_length_v = 1024 * 8; using server_name_type = augs::constant_size_string; using game_mode_name_type = augs::constant_size_string; diff --git a/src/game/components/text_details_component.h b/src/game/components/text_details_component.h index 143d698cbd..be4b241a8c 100644 --- a/src/game/components/text_details_component.h +++ b/src/game/components/text_details_component.h @@ -6,6 +6,7 @@ namespace invariants { static constexpr bool allow_nontriviality = true; // GEN INTROSPECTOR struct invariants::text_details + entity_name_str resource_id; entity_name_str name; entity_name_str description; // END GEN INTROSPECTOR diff --git a/src/game/detail/inventory/generate_equipment.h b/src/game/detail/inventory/generate_equipment.h index fb02e43753..c7ca8a7174 100644 --- a/src/game/detail/inventory/generate_equipment.h +++ b/src/game/detail/inventory/generate_equipment.h @@ -184,91 +184,107 @@ entity_id requested_equipment::generate_for_impl( entity_id result_weapon; - if (eq.weapon.is_set()) { - if (const auto weapon = make_owned_item(eq.weapon)) { - result_weapon = weapon.get_id(); - - /* So that the effect transform is valid */ - weapon.set_logic_transform(character_transform); - - const auto chamber_slot = weapon[slot_function::GUN_CHAMBER]; - const auto chamber_mag_slot = weapon[slot_function::GUN_CHAMBER_MAGAZINE]; + auto fill_with_ammo = [&](const auto& weapon) { + const auto chamber_slot = weapon[slot_function::GUN_CHAMBER]; + const auto chamber_mag_slot = weapon[slot_function::GUN_CHAMBER_MAGAZINE]; - auto create_charge_in_chamber = [&](const auto& charge_flavour) { - if (charge_flavour.is_set()) { - if (chamber_slot) { - if (const auto c = make_owned_item(charge_flavour)) { - c.set_charges(1); + auto create_charge_in_chamber = [&](const auto& charge_flavour) { + if (charge_flavour.is_set()) { + if (chamber_slot) { + if (const auto c = make_owned_item(charge_flavour)) { + c.set_charges(1); - transfer(c, chamber_slot, false); + transfer(c, chamber_slot, false); - if (chamber_mag_slot) { - if (const auto num_fitting = c.num_charges_fitting_in(chamber_mag_slot); num_fitting > 0) { - if (const auto cm = make_owned_item(charge_flavour)) { - cm.set_charges(num_fitting); + if (chamber_mag_slot) { + if (const auto num_fitting = c.num_charges_fitting_in(chamber_mag_slot); num_fitting > 0) { + if (const auto cm = make_owned_item(charge_flavour)) { + cm.set_charges(num_fitting); - transfer(cm, chamber_mag_slot, false); - } + transfer(cm, chamber_mag_slot, false); } } } } } - }; - - const auto magazine_slot = weapon[slot_function::GUN_DETACHABLE_MAGAZINE]; - - if (magazine_slot) { - const auto final_mag_flavour = [&]() { - if (eq.non_standard_mag.is_set()) { - return eq.non_standard_mag; - } + } + }; - return magazine_slot->only_allow_flavour; - }(); + const auto magazine_slot = weapon[slot_function::GUN_DETACHABLE_MAGAZINE]; - if (ammo_pieces_to_generate_left < 0) { - ammo_pieces_to_generate_left = weapon.template get().gratis_ammo_pieces_with_first; + if (magazine_slot) { + const auto final_mag_flavour = [&]() { + if (eq.non_standard_mag.is_set()) { + return eq.non_standard_mag; } - if (ammo_pieces_to_generate_left > 0) { - if (const auto new_mag = make_ammo_piece(final_mag_flavour)) { - transfer(new_mag, magazine_slot, false); - --ammo_pieces_to_generate_left; + return magazine_slot->only_allow_flavour; + }(); - if (const auto loaded_charge = new_mag[slot_function::ITEM_DEPOSIT].get_item_if_any()) { - create_charge_in_chamber(loaded_charge.get_flavour_id()); - } + if (ammo_pieces_to_generate_left < 0) { + ammo_pieces_to_generate_left = weapon.template get().gratis_ammo_pieces_with_first; + } + + if (ammo_pieces_to_generate_left > 0) { + if (const auto new_mag = make_ammo_piece(final_mag_flavour)) { + transfer(new_mag, magazine_slot, false); + --ammo_pieces_to_generate_left; - generate_spares(final_mag_flavour); + if (const auto loaded_charge = new_mag[slot_function::ITEM_DEPOSIT].get_item_if_any()) { + create_charge_in_chamber(loaded_charge.get_flavour_id()); } + + generate_spares(final_mag_flavour); } } - else if (chamber_slot) { - const auto final_charge_flavour = [&]() { - if (eq.non_standard_mag.is_set()) { - return eq.non_standard_charge; - } + } + else if (chamber_slot) { + const auto final_charge_flavour = [&]() { + if (eq.non_standard_mag.is_set()) { + return eq.non_standard_charge; + } - return chamber_slot->only_allow_flavour; - }(); + return chamber_slot->only_allow_flavour; + }(); - if (ammo_pieces_to_generate_left < 0) { - ammo_pieces_to_generate_left = weapon.template get().gratis_ammo_pieces_with_first; - } + if (ammo_pieces_to_generate_left < 0) { + ammo_pieces_to_generate_left = weapon.template get().gratis_ammo_pieces_with_first; + } - if (ammo_pieces_to_generate_left > 0) { - create_charge_in_chamber(final_charge_flavour); - --ammo_pieces_to_generate_left; + if (ammo_pieces_to_generate_left > 0) { + create_charge_in_chamber(final_charge_flavour); + --ammo_pieces_to_generate_left; - generate_spares(final_charge_flavour); - } + generate_spares(final_charge_flavour); } + } + }; + + if (eq.weapon.is_set()) { + if (const auto weapon = make_owned_item(eq.weapon)) { + result_weapon = weapon.get_id(); + + /* So that the effect transform is valid */ + weapon.set_logic_transform(character_transform); + + fill_with_ammo(weapon); if constexpr(!to_the_ground) { pickup(weapon); } } + + if (eq.weapon_secondary.is_set()) { + if (const auto weapon = make_owned_item(eq.weapon_secondary)) { + weapon.set_logic_transform(character_transform); + + fill_with_ammo(weapon); + + if constexpr(!to_the_ground) { + transfer(weapon, character[slot_function::SECONDARY_HAND]); + } + } + } } else { if (const auto& f = eq.non_standard_mag; f.is_set()) { diff --git a/src/game/detail/inventory/requested_equipment.h b/src/game/detail/inventory/requested_equipment.h index 1651bd7841..dcad5f139c 100644 --- a/src/game/detail/inventory/requested_equipment.h +++ b/src/game/detail/inventory/requested_equipment.h @@ -4,7 +4,7 @@ #include "game/cosmos/step_declaration.h" #include "game/detail/spells/all_spells.h" -using other_equipment_vector = std::vector>; +using other_equipment_vector = augs::constant_size_vector, 10>; class cosmos; @@ -13,6 +13,7 @@ class allocate_new_entity_access; struct requested_equipment { // GEN INTROSPECTOR struct requested_equipment item_flavour_id weapon; + item_flavour_id weapon_secondary; item_flavour_id non_standard_mag; item_flavour_id non_standard_charge; int num_given_ammo_pieces = -1; diff --git a/src/game/modes/arena_mode.cpp b/src/game/modes/arena_mode.cpp index 6d8e54b1fa..3e8e6c7505 100644 --- a/src/game/modes/arena_mode.cpp +++ b/src/game/modes/arena_mode.cpp @@ -33,6 +33,9 @@ #include "game/detail/sentience/sentience_logic.h" #include "game/modes/detail/delete_with_held_items.hpp" #include "game/modes/detail/hud_message_players.h" +#include "game/modes/mode_commands/translate_game_commands.h" +#include "test_scenes/test_scene_flavour_ids.h" +#include "test_scenes/test_scene_flavours.h" using input_type = arena_mode::input; using const_input_type = arena_mode::const_input; @@ -851,7 +854,9 @@ void arena_mode::play_start_round_sound(const input_type in, const const_logic_s battle_event::START ; - if (start_event == battle_event::PREPARE_TO_FIGHT) { + const bool has_prepare_to_fight_sound = false; + + if (has_prepare_to_fight_sound && start_event == battle_event::PREPARE_TO_FIGHT) { // Custom logic: Distribute "PREPARE" sounds evenly const auto p = calc_participating_factions(in); @@ -880,7 +885,8 @@ void arena_mode::play_start_round_sound(const input_type in, const const_logic_s void arena_mode::setup_round( const input_type in, const logic_step step, - const arena_mode::round_transferred_players& transfers + const arena_mode::round_transferred_players& transfers, + const setup_next_round_params params ) { auto access = allocate_new_entity_access(); @@ -927,6 +933,10 @@ void arena_mode::setup_round( current_round = {}; + if (params.skip_freeze_time) { + current_round.skip_freeze_time = true; + } + for_each_faction([&](const auto faction) { fill_spawns(cosm, faction, factions[faction]); }); @@ -959,13 +969,14 @@ void arena_mode::setup_round( step.post_message(msg); } - if (in.rules.freeze_secs > 0.f) { + if (get_freeze_time(in) > 0.f) { if (state != arena_mode_state::WARMUP) { set_players_frozen(in, true); release_triggers_of_weapons_of_players(in); } } else { + set_players_frozen(in, false); play_start_round_sound(in, step); } @@ -1052,14 +1063,14 @@ arena_mode::round_transferred_players arena_mode::make_transferred_players(const return result; } -void arena_mode::start_next_round(const input_type in, const logic_step step, const round_start_type type) { +void arena_mode::start_next_round(const input_type in, const logic_step step, const round_start_type type, const setup_next_round_params params) { state = arena_mode_state::LIVE; if (type == round_start_type::KEEP_EQUIPMENTS) { - setup_round(in, step, make_transferred_players(in)); + setup_round(in, step, make_transferred_players(in), params); } else { - setup_round(in, step); + setup_round(in, step, {}, params); } } @@ -1082,13 +1093,17 @@ arena_mode_player_stats* arena_mode::stats_of(const mode_player_id& id) { } template -static void delete_all_owned_items(const E handle) { +static void delete_all_owned_items(const E handle, const std::optional except = std::nullopt) { deletion_queue q; auto& cosm = handle.get_cosmos(); handle.for_each_contained_item_recursive( [&](const auto& contained) { + if (except == contained.get_flavour_id()) { + return; + } + q.push_back(entity_id(contained.get_id())); } ); @@ -1841,15 +1856,130 @@ void arena_mode::scramble_assigned_factions(const arena_mode::participating_fact } } + +template +void set_specific_equipment_for(allocate_new_entity_access access, logic_step step, const requested_equipment& eq, H player_handle, F except_flavour) { + ::delete_all_owned_items(player_handle, entity_flavour_id(except_flavour)); + eq.generate_for(access, player_handle, step, 1); +} + void arena_mode::handle_special_commands(const input_type in, const mode_entropy& entropy, const logic_step step) { const auto& g = entropy.general; + auto handle_game_command = [&](const G& cmd) { + if constexpr(std::is_same_v) { + switch (cmd) { + case no_arg_game_command::ROUND_RESTART: + start_next_round(in, step); + break; + case no_arg_game_command::ROUND_RESTART_NOFREEZE: { + setup_next_round_params params; + params.skip_freeze_time = true; + + start_next_round(in, step, round_start_type::KEEP_EQUIPMENTS, params); + break; + } + case no_arg_game_command::RICH: + for (auto& p : players) { + p.second.stats.money = 1000000; + } + break; + + default: + break; + } + } + else if constexpr(std::is_same_v) { + if (auto player = find_player_by(cmd.nickname)) { + if (auto handle = in.cosm[player->second.controlled_character_id]) { + handle.set_logic_transform(cmd.new_transform); + + auto& off = handle.template get().base_offset; + off = off.length() * cmd.new_transform.get_direction(); + } + } + } + else if constexpr(std::is_same_v) { + if (auto player = find_player_by(cmd.nickname)) { + if (auto handle = in.cosm[player->second.controlled_character_id]) { + requested_equipment eq; + + eq.personal_deposit_wearable = to_entity_flavour_id(test_container_items::STANDARD_PERSONAL_DEPOSIT); + + auto faction = player->second.get_faction(); + + for (auto item_str : cmd.items) { + auto i = item_str.operator std::string(); + + if (i == "armor") { + eq.armor_wearable = to_entity_flavour_id(test_tool_items::ELECTRIC_ARMOR); + continue; + } + + if (i == "backpack") { + eq.back_wearable = to_entity_flavour_id(faction == faction_type::METROPOLIS ? test_tool_items::METROPOLIS_BACKPACK : test_tool_items::RESISTANCE_BACKPACK); + continue; + } + + bool akimbo = false; + + if (i.substr(0, 2) == "2X") { + akimbo = true; + + i.erase(i.begin()); + i.erase(i.begin()); + } + + auto item_lambda = [&](const auto flavour_id, const auto& flavour) { + if (flavour.template get().resource_id != i) { + return; + } + + auto add_to_other = [&]() { + if (eq.other_equipment.size() < eq.other_equipment.max_size()) { + eq.other_equipment.push_back({ 1, flavour_id }); + } + }; + + if (::is_weapon_like(flavour)) { + if (eq.weapon.is_set()) { + add_to_other(); + } + else { + eq.weapon = flavour_id; + + if (akimbo) { + eq.weapon_secondary = flavour_id; + } + } + } + else { + add_to_other(); + } + }; + + in.cosm.for_each_flavour_having(item_lambda); + } + + auto access = allocate_new_entity_access(); + ::set_specific_equipment_for(access, step, eq, handle, in.rules.bomb_flavour); + } + } + } + else { + static_assert(always_false_v, "Non-exhaustive"); + } + }; + std::visit( [&](const auto& cmd) { using C = remove_cref; if constexpr(std::is_same_v) { + } + else if constexpr(std::is_same_v) { + ::translate_game_commands(cmd, handle_game_command); } else if constexpr(std::is_same_v) { switch (cmd) { @@ -2822,11 +2952,11 @@ float arena_mode::get_seconds_passed_in_cosmos(const const_input_type in) const } float arena_mode::get_round_seconds_passed(const const_input_type in) const { - return get_seconds_passed_in_cosmos(in) - static_cast(in.rules.freeze_secs); + return get_seconds_passed_in_cosmos(in) - get_freeze_time(in); } float arena_mode::get_freeze_seconds_left(const const_input_type in) const { - return static_cast(in.rules.freeze_secs) - get_seconds_passed_in_cosmos(in); + return get_freeze_time(in) - get_seconds_passed_in_cosmos(in); } float arena_mode::get_buy_seconds_left(const const_input_type in) const { @@ -2846,11 +2976,11 @@ float arena_mode::get_buy_seconds_left(const const_input_type in) const { return 0.f; } - return static_cast(in.rules.freeze_secs + in.rules.buy_secs_after_freeze) - get_seconds_passed_in_cosmos(in); + return static_cast(get_freeze_time(in) + in.rules.buy_secs_after_freeze) - get_seconds_passed_in_cosmos(in); } float arena_mode::get_round_seconds_left(const const_input_type in) const { - return static_cast(in.rules.round_secs) + in.rules.freeze_secs - get_seconds_passed_in_cosmos(in); + return static_cast(in.rules.round_secs) + get_freeze_time(in) - get_seconds_passed_in_cosmos(in); } float arena_mode::get_seconds_since_win(const const_input_type in) const { @@ -3057,20 +3187,12 @@ const arena_mode_player* arena_mode::find(const session_id_type& session_id) con return nullptr; } -arena_mode_player* arena_mode::find_player_by(const client_nickname_type& nickname) { - if (const auto r = find_player_by_impl(*this, nickname)) { - return std::addressof(r->second); - } - - return nullptr; +arena_mode::player_entry_type* arena_mode::find_player_by(const client_nickname_type& nickname) { + return find_player_by_impl(*this, nickname); } -const arena_mode_player* arena_mode::find_player_by(const client_nickname_type& nickname) const { - if (const auto r = find_player_by_impl(*this, nickname)) { - return std::addressof(r->second); - } - - return nullptr; +const arena_mode::player_entry_type* arena_mode::find_player_by(const client_nickname_type& nickname) const { + return find_player_by_impl(*this, nickname); } void arena_mode::restart_match(const input_type in, const logic_step step) { @@ -3286,7 +3408,7 @@ mode_player_id arena_mode::get_next_to_spectate( // TODO: Optimize if (const auto player = find_player_by(it->nickname)) { - return lookup(player->controlled_character_id); + return lookup(player->second.controlled_character_id); } return {}; @@ -3403,3 +3525,7 @@ bool arena_mode_ruleset::is_ffa() const { bool arena_mode::levelling_enabled(const_input_type in) const { return state != arena_mode_state::WARMUP && std::holds_alternative(in.rules.subrules); } + +float arena_mode::get_freeze_time(const const_input_type in) const { + return current_round.skip_freeze_time ? 0.0f : in.rules.freeze_secs; +} diff --git a/src/game/modes/arena_mode.h b/src/game/modes/arena_mode.h index a7e05c48ea..2fd4ea3b35 100644 --- a/src/game/modes/arena_mode.h +++ b/src/game/modes/arena_mode.h @@ -188,6 +188,7 @@ struct arena_mode_player { struct arena_mode_round_state { // GEN INTROSPECTOR struct arena_mode_round_state bool cache_players_frozen = false; + bool skip_freeze_time = false; arena_mode_win last_win; arena_mode_knockouts_vector knockouts; mode_player_id bomb_planter; @@ -201,6 +202,10 @@ enum class round_start_type { struct debugger_property_accessors; +struct setup_next_round_params { + bool skip_freeze_time = false; +}; + class arena_mode { public: using ruleset_type = arena_mode_ruleset; @@ -321,8 +326,8 @@ class arena_mode { void mode_pre_solve(input, const mode_entropy&, logic_step); void mode_post_solve(input, const mode_entropy&, logic_step); - void start_next_round(input, logic_step, round_start_type = round_start_type::KEEP_EQUIPMENTS); - void setup_round(input, logic_step, const round_transferred_players& = {}); + void start_next_round(input, logic_step, round_start_type = round_start_type::KEEP_EQUIPMENTS, setup_next_round_params = {}); + void setup_round(input, logic_step, const round_transferred_players& = {}, setup_next_round_params = {}); void reshuffle_spawns(const cosmos&, arena_mode_faction_state&); void fill_spawns(const cosmos&, faction_type, arena_mode_faction_state& out); @@ -470,9 +475,11 @@ class arena_mode { unsigned get_score(faction_type) const; - player_type* find_player_by(const client_nickname_type& nickname); + using player_entry_type = decltype(players)::value_type; + + player_entry_type* find_player_by(const client_nickname_type& nickname); player_type* find(const mode_player_id&); - const player_type* find_player_by(const client_nickname_type& nickname) const; + const player_entry_type* find_player_by(const client_nickname_type& nickname) const; const player_type* find(const mode_player_id&) const; const player_type* find(const session_id_type&) const; @@ -618,4 +625,6 @@ class arena_mode { bool levelling_enabled(const_input) const; arena_mode_faction_state& get_spawns_for(input, faction_type faction); + + float get_freeze_time(const_input) const; }; diff --git a/src/game/modes/mode_commands/mode_entropy_structs.h b/src/game/modes/mode_commands/mode_entropy_structs.h index 99db20f19b..e1e9ad33ae 100644 --- a/src/game/modes/mode_commands/mode_entropy_structs.h +++ b/src/game/modes/mode_commands/mode_entropy_structs.h @@ -23,7 +23,10 @@ struct add_player_input { } }; +using custom_game_commands_string_type = std::string; + using all_general_mode_commands_variant = std::variant< std::monostate, - match_command + match_command, + custom_game_commands_string_type >; diff --git a/src/game/modes/mode_commands/translate_game_commands.h b/src/game/modes/mode_commands/translate_game_commands.h new file mode 100644 index 0000000000..9ba74a1d66 --- /dev/null +++ b/src/game/modes/mode_commands/translate_game_commands.h @@ -0,0 +1,132 @@ +#pragma once +#include "augs/network/network_types.h" + +/* + round_restart + round_restart_nofreeze + rich + + etc., just lowercase versions +*/ + +enum class no_arg_game_command { + ROUND_RESTART, + ROUND_RESTART_NOFREEZE, + RICH +}; + +/* + setpos nickname x y [angle] + examples: + + setpos "Pythagoras" 20 10 + setpos Pythagoras 20 10 90 +*/ + +struct setpos_game_command { + client_nickname_type nickname; + transformr new_transform; +}; + +/* + seteq nickname items... + examples: + + seteq "Pythagoras" backpack armor + seteq Pythagoras baka47 szturm backpack force_grenade + seteq Pythagoras + + (can be empty too) +*/ + +struct seteq_game_command { + client_nickname_type nickname; + augs::constant_size_vector, 20> items; +}; + +/* + all_commands is a line-breaked sequence of commands, e.g. + + round_restart + setpos Pythagoras 10 10 45 + rich + setpos "cold dimensions" 505 5 + setpos Pythagoras 0 0 + + Nickname will or will not be delimited by ", depending if it has a space. If there's no ", + assume all characters until the next space consitute the nickname. + + callback is a generic lambda and will be called on seteq_game_command, + setpos_game_command or no_arg_game_command respectively depending on the type of command. +*/ + +template +void translate_game_commands( + const S& all_commands, + F&& callback +) { + std::istringstream stream(all_commands); + std::string line; + + // Lambda to read a nickname + auto read_nickname = [](std::istringstream& stream) -> client_nickname_type { + std::string nickname; + + stream >> std::ws; + + if (stream.peek() == '"') { + stream.get(); // Skip the opening quote + std::getline(stream, nickname, '"'); + // Skip the space after closing quote + stream >> std::ws; + } + else { + stream >> nickname; + } + return nickname; + }; + + while (std::getline(stream, line)) { + std::istringstream line_stream(line); + std::string command; + line_stream >> command; + + if (command == "round_restart") { + callback(no_arg_game_command::ROUND_RESTART); + } + else if (command == "round_restart_nofreeze") { + callback(no_arg_game_command::ROUND_RESTART_NOFREEZE); + } + else if (command == "rich") { + callback(no_arg_game_command::RICH); + } + else if (command == "setpos") { + setpos_game_command cmd; + cmd.nickname = read_nickname(line_stream); + + // Read transform + line_stream >> cmd.new_transform.pos.x >> cmd.new_transform.pos.y; + if (!(line_stream >> cmd.new_transform.rotation)) { + cmd.new_transform.rotation = 0; // Default angle if not provided + } + + callback(cmd); + } + else if (command == "seteq") { + seteq_game_command cmd; + cmd.nickname = read_nickname(line_stream); + + // Read items + std::string item; + while (line_stream >> item) { + if (cmd.items.size() >= cmd.items.max_size()) { + break; + } + + cmd.items.push_back(item); + } + + callback(cmd); + } + } +} diff --git a/src/test_scenes/test_scene_flavours.h b/src/test_scenes/test_scene_flavours.h index c6c10c2b86..898d5cb250 100644 --- a/src/test_scenes/test_scene_flavours.h +++ b/src/test_scenes/test_scene_flavours.h @@ -95,6 +95,7 @@ auto& get_test_flavour(all_entity_flavours& flavours, const T enum_id) { const auto flavour_id = to_raw_flavour_id(enum_id); auto& new_flavour = into[flavour_id]; new_flavour.template get().name = format_enum(enum_id); + new_flavour.template get().resource_id = to_lowercase(augs::enum_to_string(enum_id)); if constexpr(std::is_same_v) { new_flavour.template get().basic_penetration_distance = get_penetration(enum_id);