#include "Engine/Game/World/world_names.h" #include "Extension/Console/commands.h" #include "Extension/Profile/local_profile_runtime.h" #include "Extension/Multiplayer/Session/session.h" #include #include #include namespace dingosdk::console { namespace { constexpr std::string_view base_level_asset = "Levels/Game/DingoLevel_Root/DingoLevel_Root"; const overlay::Level *base_level(const Model &model) { for (const auto &level : model.levels) if (equal(level.asset, base_level_asset)) return &level; return nullptr; } // On a dedicated server, its admins choose from the maps that server has // (its retail maps and the custom maps in its Mods folder), not this PC's. using ServerMaps = std::optional>; ServerMaps admin_server_maps() { auto session = multiplayer::model(); if (!session.server_admin || session.server_maps.empty()) return std::nullopt; return std::move(session.server_maps); } // A destination `levels`, `load` and Tab offer: never the base root, and on a // dedicated server only its maps. bool offered(const overlay::Level &level, const ServerMaps &maps) { if (equal(level.asset, base_level_asset)) return false; return !maps || std::any_of(maps->begin(), maps->end(), [&](const auto &asset) { return equal(asset, level.asset); }); } std::string destination_name(const overlay::Level &level) { if (!level.display_name.empty()) return level.display_name; return world_level_name(level.asset); } const overlay::Level *find_destination(const Model &model, std::string_view name, const ServerMaps &maps) { if (name.empty()) return nullptr; std::size_t number{}; const auto parsed = std::from_chars(name.data(), name.data() + name.size(), number); if (parsed.ec == std::errc{} && parsed.ptr == name.data() + name.size()) { std::size_t index = 0; for (const auto &level : model.levels) if (offered(level, maps) && ++index == number) return &level; return nullptr; } for (const auto &level : model.levels) if (offered(level, maps) && equal(level.asset, name)) return &level; for (const bool exact : {true, false}) { const overlay::Level *result = nullptr; for (const auto &level : model.levels) { if (!offered(level, maps)) continue; const auto label = destination_name(level); const auto alias = world_level_short_name(level.asset); if (exact ? (equal(label, name) || equal(alias, name)) : (ascii_case_insensitive_starts_with(label, name) || ascii_case_insensitive_starts_with(alias, name) || ascii_case_insensitive_starts_with(level.asset, name))) { if (result) return nullptr; result = &level; } } if (result) return result; } return nullptr; } } // namespace void register_world_commands(Commands ®istry) { auto levels = action("levels", "List destinations and their one-based indices", Group::world); levels.execution = Execution::local; levels.run = [](const Model &m, const Values &, const Output &out) { const auto maps = admin_server_maps(); if (maps) out("The server's maps:"); std::size_t index = 0; for (const auto &level : m.levels) if (offered(level, maps)) out(std::to_string(++index) + ": " + destination_name(level) + " - " + level.asset); if (!index) out(maps ? "You have none of the server's maps installed." : "No destinations have been discovered yet."); }; registry.add(std::move(levels)); auto destination = argument("destination|index"); destination.complete = [](const Model &m, auto) { const auto maps = admin_server_maps(); std::vector result; for (const auto &level : m.levels) { if (!offered(level, maps)) continue; const auto name = destination_name(level); result.push_back(find_destination(m, name, maps) == &level ? name : level.asset); } return result; }; auto start = argument("start|-", Type::text, true); start.complete = [](const Model &m, auto args) { std::vector result{"-"}; if (!args.empty()) if (const auto *level = find_destination(m, args[0], admin_server_maps())) result.insert(result.end(), level->start_points.begin(), level->start_points.end()); return result; }; auto load = action("load", "Load a destination with the base root supplied automatically", Group::world, {destination, start}); load.inspect = [](const Model &m) { const auto *root = base_level(m); const bool ready = root && root->native_registered && root->can_load; return State{ ready && m.can_queue_load, {}, ready ? m.load_block_reason : "The base level is not ready.", {}, false}; }; load.run = [](const Model &m, const Values &args, const Output &out) { const auto *root = base_level(m); const auto maps = admin_server_maps(); const auto *level = find_destination(m, std::get(args[0]), maps); if (!root || !root->native_registered || !root->can_load) { out("error: The base level is not ready."); return; } if (!level) { out(maps ? "error: The server doesn't have that map (or you don't); use levels or Tab." : "error: Destination is missing or ambiguous; use levels or Tab."); return; } // A dedicated server's admin moves the whole server; everyone loads it together. if (multiplayer::model().server_admin) { const bool sent = multiplayer::queue_command("server", "map " + root->asset + "|" + level->asset, ""); out(sent ? "Asked the server to change map." : "error: The request queue is busy; try again."); return; } if (!level->can_load) { out("error: " + (level->load_block_reason.empty() ? "This destination is unavailable." : level->load_block_reason)); return; } std::string spawn = args.size() > 1 ? std::get(args[1]) : ""; if (spawn == "-") spawn.clear(); if (spawn.empty()) if (const auto *automatic = level->automatic_start_point()) spawn = *automatic; if ((!spawn.empty() && std::find(level->start_points.begin(), level->start_points.end(), spawn) == level->start_points.end()) || (spawn.empty() && !level->native_registered && !level->start_points.empty())) { out("error: Choose a start point from this destination; use Tab."); return; } request_level(root->asset, "", level->asset, spawn); }; registry.add(std::move(load)); for (std::size_t i = 0; i < world_layers().size(); ++i) { const auto &layer = world_layers()[i]; auto mode = argument("default|on|off"); mode.choices = {"default", "on", "off"}; auto entry = variable("world " + std::string(layer.key), std::string(layer.detail), Group::world, mode); entry.inspect = [i](const Model &m) { return State{ m.world.available, m.world.enabled[i] ? std::optional(*m.world.enabled[i] ? "on" : "off") : std::nullopt, m.world.status[i].empty() ? "This layer is unavailable in the current level." : m.world.status[i], "Saved selection: " + m.world.choices[i] + (m.world.ready && m.world.supported[i] ? "" : "; live state unavailable in this level"), m.world.choices[i] != "default"}; }; entry.run = [i](const Model &, const Values &args, const Output &out) { // While a dedicated server syncs layers, its admins change them for everyone. if (multiplayer::model().server_admin && local_profile_world_layers().controlled_by_host) { const bool sent = multiplayer::queue_command( "server", "layer " + world_layers()[i].key + " " + std::get(args[0]), ""); out(sent ? "Sent to the server." : "error: The request queue is busy; try again."); return; } const bool saved = set_local_world_layer(world_layers()[i].key, std::get(args[0])); out((saved ? "" : "error: ") + local_profile_world_layers().feedback); }; entry.reset = [i](const Model &, const Output &out) { const bool saved = set_local_world_layer(world_layers()[i].key, "default"); out((saved ? "" : "error: ") + local_profile_world_layers().feedback); }; registry.add(std::move(entry)); } // Time of day: each map has seven time layers, "_tod__"; one is forced on // and the rest off, or all seven go back to the level's own choice. static constexpr std::array times{"default", "morning", "noon", "afternoon", "evening", "night", "weatherday", "weathernight"}; auto time = argument("default|morning|noon|afternoon|evening|night|weatherday|weathernight"); time.choices.assign(times.begin(), times.end()); auto tod = variable("tod", "Time of day on the current map", Group::world, time); // The current map's time layers in slot order, or empty when it has none. const auto time_layers = [](const Model &m) { std::vector layers; for (unsigned slot = 1; slot < times.size(); ++slot) { const auto prefix = std::string(world_map_key(m.world.map)) + "_tod_" + std::to_string(slot) + "_"; const auto found = std::find_if(world_layers().begin(), world_layers().end(), [&](const auto &layer) { return layer.key.starts_with(prefix); }); if (m.world.map == WorldMap::none || found == world_layers().end()) return std::vector{}; layers.push_back(static_cast(found - world_layers().begin())); } return layers; }; tod.inspect = [time_layers](const Model &m) { const auto layers = time_layers(m); std::size_t forced{}; for (std::size_t i = 0; i < layers.size(); ++i) { const auto &choice = m.world.choices[layers[i]]; if (choice == "on") forced = forced ? times.size() : i + 1; else if (choice != "off") forced = times.size(); } const auto value = layers.empty() || forced >= times.size() ? std::string("default") : std::string(times[forced]); return State{m.world.available && !layers.empty(), value, m.world.map == WorldMap::none ? "Load a map first." : "This map has no time-of-day layers.", "Saved for this map", value != "default"}; }; tod.run = [time_layers](const Model &m, const Values &args, const Output &out) { const auto wanted = lower(std::get(args[0])); const auto found = std::find(times.begin(), times.end(), wanted); if (found == times.end()) { out("error: Choose default, morning, noon, afternoon, evening, night, weatherday or weathernight."); return; } const auto slot = static_cast(found - times.begin()); // While a dedicated server syncs layers, its admins set the time for everyone. if (multiplayer::model().server_admin && local_profile_world_layers().controlled_by_host) { const bool sent = multiplayer::queue_command("server", "tod " + wanted, ""); out(sent ? "Sent to the server." : "error: The request queue is busy; try again."); return; } const auto layers = time_layers(m); if (layers.empty()) { out(m.world.map == WorldMap::none ? "error: Load a map first." : "error: This map has no time-of-day layers."); return; } // The others switch off before the new one comes on, so two never load together. bool saved = true; for (std::size_t i = 0; i < layers.size(); ++i) if (i + 1 != slot) saved = saved && set_local_world_layer(world_layers()[layers[i]].key, slot ? "off" : "default"); if (slot) saved = saved && set_local_world_layer(world_layers()[layers[slot - 1]].key, "on"); out(saved ? "Time of day: " + wanted + "." : "error: " + local_profile_world_layers().feedback); }; registry.add(std::move(tod)); auto map = argument("map"); for (const auto anchor : world_map_anchors()) map.choices.emplace_back(world_map_key(world_layer_nodes()[anchor].map)); auto defaults = action("world defaults", "Restore authored world layers for a map", Group::world, {map}); defaults.run = [](const Model &, const Values &args, const Output &out) { const bool saved = restore_local_world_layers(std::get(args[0])); out((saved ? "" : "error: ") + local_profile_world_layers().feedback); }; registry.add(std::move(defaults)); for (unsigned lot = 0; lot < park_lots.size(); ++lot) { auto layout = argument("layout|empty"); layout.choices = {"empty"}; for (unsigned family = 0; family < park_families.size(); ++family) for (unsigned i = 1; i <= park_lots[lot].counts[family]; ++i) layout.choices.push_back(park_id(family, i)); auto entry = variable("park " + std::string(park_lots[lot].key), "Choose the layout for " + std::string(park_lots[lot].label), Group::world, layout); entry.inspect = [lot](const Model &m) { return State{m.parks.available, m.parks.choices[lot].empty() ? "default" : m.parks.choices[lot], "Park controls are unavailable.", "Saved layout", false}; }; entry.run = [lot](const Model &, const Values &args, const Output &out) { // A lobby's parks follow its host; a dedicated server's admins choose them for everyone. if (multiplayer::model().server_admin) { const bool sent = multiplayer::queue_command( "server", "park " + std::string(park_lots[lot].key) + " " + std::get(args[0]), ""); out(sent ? "Sent to the server." : "error: The request queue is busy; try again."); return; } const bool saved = set_local_park(park_lots[lot].key, std::get(args[0])); out((saved ? "" : "error: ") + local_profile_parks().feedback); }; registry.add(std::move(entry)); } struct Control { const char *key; const char *description; double maximum; double (*read)(const WorldControls &); bool population; }; const Control controls[]{ {"traffic", "Traffic density: -1 default, 0 off, 1 light, 2 normal, 3 busy", 3, [](const WorldControls &c) { return double(c.traffic); }, true}, {"pedestrians", "Pedestrian density: -1 default, 0 off, 1 light, 2 normal, 3 busy", 3, [](const WorldControls &c) { return double(c.pedestrians); }, true}, {"fog", "Fog: -1 default, 0 off, 1 on", 1, [](const WorldControls &c) { return double(c.fog); }, false}, {"fog_distance", "Fog visibility multiplier; 0.1 to 5, or -1 for default", 5, [](const WorldControls &c) { return double(c.fog_distance); }, false}, {"sky_brightness", "Sky brightness multiplier; 0 to 3, or -1 for default", 3, [](const WorldControls &c) { return double(c.sky_brightness); }, false}, {"clouds", "Cloud opacity multiplier; 0 to 2, or -1 for default", 2, [](const WorldControls &c) { return double(c.clouds); }, false}, {"wind_strength", "Wind strength; 0 to 30, or -1 for default", 30, [](const WorldControls &c) { return double(c.wind_strength); }, false}, {"wind_direction", "Wind direction in degrees; 0 to 360, or -1 for default", 360, [](const WorldControls &c) { return double(c.wind_direction); }, false}}; for (const auto &control : controls) { auto value = argument("value|-1", Type::number); value.minimum = -1; value.maximum = control.maximum; if (control.population) value.choices = {"-1", "0", "1", "2", "3"}; if (equal(control.key, "fog")) value.choices = {"-1", "0", "1"}; auto entry = variable(control.key, control.description, Group::world, value); entry.aliases = {"environment " + std::string(control.key)}; entry.inspect = [control](const Model &m) { const auto v = control.read(m.world_controls.choices); return State{ control.population ? m.world_controls.population_available : m.world_controls.environment_available, v == -1 ? "default" : value_text(v), "World controls are unavailable.", "Saved override", v != -1}; }; entry.run = [control](const Model &, const Values &args, const Output &out) { const bool saved = set_local_world_control(control.key, static_cast(std::get(args[0]))); out(saved ? local_profile_world_controls().status : "error: Invalid world value, unavailable control, or save failed."); }; entry.reset = [control](const Model &, const Output &out) { const bool saved = set_local_world_control(control.key, -1); out(saved ? local_profile_world_controls().status : "error: World control could not be restored."); }; registry.add(std::move(entry)); } for (unsigned i=0;i1 ? "x,y,z,w|default" : "value|default"); input.complete=[i](const Model& m, auto) { std::vector result{"default"}; const auto& c=dingosdk::atmosphere_controls[i]; if (c.type==AtmosphereType::texture) result.insert(result.end(),m.world_controls.textures[i].begin(),m.world_controls.textures[i].end()); if (c.type==AtmosphereType::toggle) { result.push_back("0");result.push_back("1"); } for (unsigned j=0;j current; if (reading.value) current=atmosphere_value_text(c,*reading.value); const auto saved=w.choices.atmosphere.find(c.key); std::string detail=reading.varies ? "Values vary across the level's environments. " : ""; if (saved!=w.choices.atmosphere.end()) detail+="Custom: "+atmosphere_value_text(c,saved->second)+(reading.applied ? "" : " (pending)"); else detail+="Authored values / active convenience commands"; return State{w.environment_available,current,"Environment bindings are unavailable.",detail,saved!=w.choices.atmosphere.end()}; }; entry.run=[i](const Model&,const Values& values,const Output& out) { const bool ok=set_local_atmosphere_control(dingosdk::atmosphere_controls[i].key,std::get(values[0])); out(ok ? local_profile_world_controls().status : "error: Invalid value, texture not loaded, or save failed."); }; entry.reset=[i](const Model&,const Output& out) { out(set_local_atmosphere_control(dingosdk::atmosphere_controls[i].key,"default") ? local_profile_world_controls().status : "error: Could not restore this control."); }; registry.add(std::move(entry)); } for (unsigned kind=0;kind<3;++kind) { auto entry=action(std::string(atmosphere_groups[kind])+".reset","Restore this group's authored values",Group::world); entry.run=[kind](const Model&,const Values&,const Output& out) { out(reset_local_atmosphere_controls(kind) ? local_profile_world_controls().status : "error: Could not restore this group."); }; registry.add(std::move(entry)); } auto reset_value = argument("-1", Type::number); reset_value.choices = {"-1"}; reset_value.optional = true; auto environment_reset = action("environment reset_environment", "Restore all authored atmosphere settings", Group::world, {reset_value}); environment_reset.run = [](const Model &, const Values &, const Output &out) { out(set_local_world_control("reset_environment", -1) ? local_profile_world_controls().status : "error: Could not restore environment."); }; registry.add(std::move(environment_reset)); } } // namespace dingosdk::console