#pragma once #include "Extension/UI/Overlay/overlay.h" #include "Engine/Game/World/world_names.h" #include #include #include #include namespace dingosdk::native_tools { inline constexpr std::string_view root_asset = "Levels/Game/DingoLevel_Root/DingoLevel_Root"; inline constexpr std::array sections{"World", "Custom Maps", "Parks", "Player", "Visuals"}; enum Section : unsigned { world_section, custom_section, parks_section, player_section, visuals_section }; // The ReSkate splash level only hosts the startup screen; it is never a destination. inline constexpr std::string_view splash_asset = "DingoLevel_Splash"; struct State { std::string destination, feedback; unsigned lot{}; ParkChoices parks, seen_parks; }; struct Row { std::string id, title, command, argument; bool button{}, primary{}; bool input{}; // title: placeholder, argument: initial text. }; struct Page { std::vector main, side; }; inline bool same(std::string_view a, std::string_view b) { return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin(), [](unsigned char x, unsigned char y) { return std::tolower(x) == std::tolower(y); }); } inline const overlay::Level* level(const overlay::Model& model, std::string_view asset) { const auto it = std::find_if(model.levels.begin(), model.levels.end(), [&](const auto& entry) { return same(entry.asset, asset); }); return it == model.levels.end() ? nullptr : &*it; } inline bool listed(const overlay::Level& entry) { if (same(entry.asset, root_asset)) return false; const auto name = std::string_view(entry.asset).substr(entry.asset.find_last_of("/\\") + 1); return !same(name, splash_asset); } inline std::string level_name(const overlay::Level& entry) { return entry.display_name.empty() ? world_level_name(entry.asset) : entry.display_name; } inline std::vector layouts(unsigned lot) { std::vector result{"empty"}; for (unsigned family = 0; family < park_families.size(); ++family) for (unsigned variant = 1; variant <= park_lots[lot].counts[family]; ++variant) result.push_back(park_id(family, variant)); return result; } inline void sync(State& s, const overlay::Model& model) { if (!level(model, s.destination)) s.destination.clear(); for (unsigned i = 0; i < park_lots.size(); ++i) { if (s.parks[i].empty() || s.seen_parks[i] != model.parks.choices[i]) { s.seen_parks[i] = model.parks.choices[i]; s.parks[i] = valid_park(i, s.seen_parks[i]) && !s.seen_parks[i].empty() ? s.seen_parks[i] : "empty"; } } } inline bool can_load(const State& s, const overlay::Model& m, const overlay::CallbacksV3& cb) { const auto* root = level(m, root_asset); const auto* selected = level(m, s.destination); return root && selected && root != selected && root->native_registered && root->can_load && selected->can_load && m.can_queue_load && !multiplayer_controls_level(m.multiplayer) && cb.queue_load; } // Layers and parks the host controls are locked, except for a dedicated // server's admins: their changes go to the server, which applies them to everyone. // (On a dedicated server: unless the player's role has the command that changes them for everyone.) inline bool layers_locked(const overlay::Model& m) { return m.world.controlled_by_host && !server_can(m.multiplayer, "layers"); } inline bool parks_locked(const overlay::Model& m) { return m.parks.controlled_by_host && !server_can(m.multiplayer, "park"); } inline bool can_park(const overlay::Model& m, const overlay::CallbacksV3& cb) { return m.world.map == WorldMap::bam && m.parks.available && !parks_locked(m) && cb.queue_console_command; } // Time of day is a group of seven world layers per map, "_tod_". The // selector forces one on and the others off; Default returns all seven to the // level's own choice. inline constexpr std::array time_of_day_keys{ "1_morning", "2_noon", "3_afternoon", "4_evening", "5_night", "6_weatherday", "7_weathernight"}; inline constexpr std::array time_of_day_labels{ "Default", "Morning", "Noon", "Afternoon", "Evening", "Night", "Weather (day)", "Weather (night)"}; inline std::size_t time_of_day_layer(WorldMap map, unsigned slot) { const auto key = std::string(world_map_key(map)) + "_tod_" + time_of_day_keys[slot]; for (std::size_t i = 0; i < world_layers().size(); ++i) if (world_layers()[i].key == key) return i; return world_layers().size(); } inline bool time_of_day_available(const overlay::Model& m, const overlay::CallbacksV3& cb) { if (!m.world.available || !m.world.ready || layers_locked(m) || !cb.queue_console_command) return false; for (unsigned slot = 0; slot < time_of_day_keys.size(); ++slot) { const auto layer = time_of_day_layer(m.world.map, slot); if (layer == world_layers().size() || !m.world.supported[layer]) return false; } return true; } // 0 while no single time is forced, otherwise 1..7. inline unsigned time_of_day(const overlay::Model& m) { unsigned forced{}; for (unsigned slot = 0; slot < time_of_day_keys.size(); ++slot) { const auto layer = time_of_day_layer(m.world.map, slot); if (layer == world_layers().size()) return 0; const auto& choice = m.world.choices[layer]; if (choice == "on") { if (forced) return 0; forced = slot + 1; } else if (choice != "off") return 0; } return forced; } // The console lines that select a time of day (0: the level's own). The others // switch off before the new one comes on, so two never load together. A // dedicated server's admin sends them to the server as one request. inline std::vector time_of_day_commands(const overlay::Model& m, unsigned next) { std::vector commands, pairs; for (unsigned slot = 0; slot < time_of_day_keys.size(); ++slot) { const auto mode = !next ? "default" : slot + 1 == next ? "on" : "off"; const auto& key = world_layers()[time_of_day_layer(m.world.map, slot)].key; const auto line = "world " + std::string(key) + " " + mode; if (slot + 1 == next) { commands.push_back(line); pairs.push_back(std::string(key) + "=" + mode); } else { commands.insert(commands.begin(), line); pairs.insert(pairs.begin(), std::string(key) + "=" + mode); } } if (server_can(m.multiplayer, "layers") && m.world.controlled_by_host) { std::string batch = "mp server layers"; for (const auto& pair : pairs) batch += " " + pair; return {batch}; } return commands; } inline void cycle(std::string& current, const std::vector& choices) { if (choices.empty()) return; const auto at = std::find(choices.begin(), choices.end(), current); current = at == choices.end() || at + 1 == choices.end() ? choices.front() : *(at + 1); } // Run outside the native model lock. Every activation rechecks the latest // runtime snapshot, including level transitions and multiplayer host ownership. inline void activate(State& s, const overlay::Model& m, const overlay::CallbacksV3& cb, std::string_view command, const std::string& argument) { sync(s, m); s.feedback.clear(); std::array result{}; const auto report = [&](bool accepted) { result.back() = '\0'; s.feedback = accepted ? "" : "Couldn't apply that change. Try again when the game is ready."; }; const auto console = [&](const std::string& text, bool enabled) { const bool accepted = enabled && cb.queue_console_command && cb.queue_console_command(cb.user, text.c_str(), result.data(), result.size()); report(accepted); return accepted; }; if (command == "select-level") { if (multiplayer_controls_level(m.multiplayer)) { s.feedback = "Only the lobby host can change levels."; return; } if (const auto* selected = level(m, argument); selected && listed(*selected)) { s.destination = selected->asset; } } else if (command == "load-level") { if (multiplayer_controls_level(m.multiplayer)) { s.feedback = "Only the lobby host can change levels."; return; } if (!can_load(s, m, cb)) { s.feedback = "Choose an available level once the game is ready."; return; } const auto* root = level(m, root_asset); const auto* selected = level(m, s.destination); const auto* point = selected->automatic_start_point(); report(cb.queue_load(cb.user, root->asset.c_str(), "", selected->asset.c_str(), point ? point->c_str() : "", result.data(), result.size())); } else if (command == "traffic" || command == "pedestrians") { const auto current = command == "traffic" ? m.world_controls.choices.traffic : m.world_controls.choices.pedestrians; console("environment " + std::string(command) + " " + std::to_string(current >= 3 ? -1 : current + 1), m.world.ready && m.world_controls.population_available); } else if (command == "time-of-day") { if (!time_of_day_available(m, cb)) { s.feedback = "Time of day is unavailable on this map right now."; return; } const auto next = static_cast((time_of_day(m) + 1) % time_of_day_labels.size()); for (const auto& line : time_of_day_commands(m, next)) console(line, true); } else if (command == "park-lot") { for (unsigned i = 0; i < park_lots.size(); ++i) if (park_lots[i].key == argument) s.lot = i; } else if (command == "park-layout") { cycle(s.parks[s.lot], layouts(s.lot)); } else if (command == "park-family") { unsigned family = 0; for (unsigned i = 0; i < park_families.size(); ++i) if (s.parks[s.lot].starts_with(park_families[i])) family = i + 1; s.parks[s.lot] = family == park_families.size() ? "empty" : park_id(family, 1); } else if (command == "load-park") { console("park " + std::string(park_lots[s.lot].key) + " " + s.parks[s.lot], can_park(m, cb) && valid_park(s.lot, s.parks[s.lot])); } else if (command == "load-random-parks") { // Use the shared command path: hosts load locally, server admins request // one server roll, and guests cannot replace the host's layouts. if (console("park random", can_park(m, cb))) s.parks = {}; } else if (command == "park-random-on-launch") { // This is a personal preference, including while following a host. console(m.parks.randomize_on_launch ? "park random-on-launch 0" : "park random-on-launch 1", m.parks.available); } else if (command == "noclip" || command == "no-bail" || command == "freecam" || command == "speed") { const auto& d = m.debug; overlay::DebugRequest request; bool enabled = false; if (command == "noclip") { request = {overlay::DebugAction::set_noclip, !d.noclip}; enabled = d.noclip_available || d.noclip; } if (command == "no-bail") { request = {overlay::DebugAction::set_no_bail, !d.no_bail}; enabled = d.no_bail_available || d.no_bail; } if (command == "freecam") { request = {overlay::DebugAction::set_free_camera, !d.free_camera}; enabled = d.available && d.camera_available; } if (command == "speed") { constexpr std::array speeds{.6f, 3.f, 5.f, 15.f, 60.f, 300.f, 1500.f}; const auto next = std::upper_bound(speeds.begin(), speeds.end(), d.camera_speed); request = {overlay::DebugAction::set_camera_speed, false, next == speeds.end() ? speeds.front() : *next}; enabled = d.camera_available && (d.free_camera || d.noclip); } report(enabled && cb.queue_debug && cb.queue_debug(cb.user, request, result.data(), result.size())); } else if (command == "board-wear") { const auto& wear = m.offline.board_wear; const overlay::OfflineFeatureRequest request{overlay::OfflineFeatureGroup::board_wear, !wear.effective}; report(wear.available && cb.queue_offline_feature && cb.queue_offline_feature(cb.user, request, result.data(), result.size())); } else if (command == "hall-of-meat") { console(m.hall_of_meat.enabled ? "hallofmeat 0" : "hallofmeat 1", m.hall_of_meat.available); } else if (command == "road-rash") { console(m.road_rash.enabled ? "roadrash 0" : "roadrash 1", m.road_rash.available); } else if (command == "road-rash-blood") { console(m.road_rash.blood ? "roadrashblood 0" : "roadrashblood 1", m.road_rash.available && m.road_rash.enabled); } else if (command == "road-rash-heal") { console("roadrash heal", m.road_rash.available && m.road_rash.enabled); } else if (command == "board-break") { console(m.board_break.enabled ? "boardbreak 0" : "boardbreak 1", m.board_break.available); } else if (command == "board-break-deck") { console("boardbreak deck next", m.board_break.available && m.board_break.has_deck); } else if (command == "board-break-always") { console(m.board_break.always ? "boardbreak always 0" : "boardbreak always 1", m.board_break.available); } else if (command == "board-break-wear") { console(m.board_break.wear ? "boardbreak wear 0" : "boardbreak wear 1", m.board_break.available); } else if (command == "grind-exits") { console(m.grind_exit.enabled ? "grindexit exits off" : "grindexit exits on", m.grind_exit.available); } else if (command == "grind-exit-sides") { console(m.grind_exit.toe_heel ? "grindexit sides travel" : "grindexit sides toeheel", m.grind_exit.available); } else if (command == "grind-exit-stance") { // Not set or regular -> goofy -> regular. console(m.grind_exit.stance == 2 ? "grindexit stance regular" : "grindexit stance goofy", m.grind_exit.available); } else if (command == "grind-exit-strength") { // 100 -> 75 -> 50 -> 25 -> 100 (the ReSkate menu's slider sets any value from 10 to 100). const int strength = m.grind_exit.strength; const int next = strength > 75 ? 75 : strength > 50 ? 50 : strength > 25 ? 25 : 100; console("grindexit strength " + std::to_string(next), m.grind_exit.available); } else if (command == "keyframe-smoothing") { console(m.keyframe_smoothing.enabled ? "keysmooth off" : "keysmooth on", m.keyframe_smoothing.available); } else if (command == "keyframe-smoothness") { // 100 -> 75 -> 50 -> 25 -> 100 (the ReSkate menu's slider sets any value from 0 to 100). const int percent = overlay::keyframe_smoothness_percent(m.keyframe_smoothing); const int next = percent > 75 ? 75 : percent > 50 ? 50 : percent > 25 ? 25 : 100; console("keysmooth amount " + std::string(next == 100 ? "1" : next == 75 ? "0.75" : next == 50 ? "0.5" : "0.25"), m.keyframe_smoothing.available && m.keyframe_smoothing.enabled); } else if (command == "board-wear-reset") { console("boardwear reset", m.offline.board_wear.available && m.offline.board_wear.effective); } else if (command == "challenges") { console(std::string("challenges ") + (m.progression.challenges_hidden ? "1" : "0"), m.progression.challenges_enabled); } else if (command == "graphics") { for (unsigned i = 0; i < graphics_keys.size(); ++i) if (argument == graphics_keys[i]) { const auto choice = m.graphics.choices.effects[i]; const bool on = choice < 0 ? !m.graphics.ready[i] || m.graphics.enabled[i] : choice != 0; console("graphics " + argument + (on ? " 0" : " 1"), m.graphics.available); } } else if (command == "graphics-reset") console("graphics reset -1", m.graphics.available); } inline Page render(State& s, const overlay::Model& m, const overlay::CallbacksV3& cb, unsigned section) { sync(s, m); Page p; const auto text = [](auto& rows, std::string id, std::string title) { if (!title.empty()) rows.push_back(Row{std::move(id), std::move(title), {}, {}, false, false}); }; const auto button = [](auto& rows, std::string id, std::string title, std::string command, bool enabled = true, std::string argument = {}, bool primary = false) { rows.push_back(Row{std::move(id), std::move(title), enabled ? std::move(command) : "", std::move(argument), true, primary}); }; const auto toggle = [&](auto& rows, const char* id, const char* title, bool on, bool enabled) { button(rows, id, std::string(title) + (on ? ": On" : ": Off"), id, enabled); }; // World lists the game's own levels, Custom Maps the ones mods declare. const auto destinations = [&](bool custom) { std::vector result; for (const auto& entry : m.levels) if (listed(entry) && entry.custom == custom) result.push_back(&entry); std::sort(result.begin(), result.end(), [](const auto* a, const auto* b) { return level_name(*a) < level_name(*b); }); for (const auto* entry : result) button(p.main, "level-" + entry->asset, level_name(*entry) + (same(entry->asset, s.destination) ? " / SELECTED" : ""), "select-level", !multiplayer_controls_level(m.multiplayer), entry->asset); return result.size(); }; const auto* selected = level(m, s.destination); const auto load_status = [&](const char* id) { if (multiplayer_controls_level(m.multiplayer)) text(p.side, id, "Only the lobby host can change levels."); else if (!m.can_queue_load || (selected && !selected->can_load)) text(p.side, id, "Travel is unavailable right now. Wait for the game to finish loading."); }; if (section == world_section) { text(p.main, "levels-title", "FIND YOUR SPOT"); if (!destinations(false)) text(p.main, "levels-empty", "Waiting for available levels..."); text(p.side, "world-title", "TRAVEL & POPULATION"); button(p.side, "load-level", "Load selected level", "load-level", can_load(s, m, cb), {}, true); for (unsigned i = 0; i < 2; ++i) { const auto choice = i == 0 ? m.world_controls.choices.traffic : m.world_controls.choices.pedestrians; button(p.side, i == 0 ? "traffic" : "pedestrians", std::string(i == 0 ? "Traffic: " : "Pedestrians: ") + population_labels[std::clamp(choice, -1, 3) + 1], i == 0 ? "traffic" : "pedestrians", m.world.ready && m.world_controls.population_available && cb.queue_console_command); } button(p.side, "time-of-day", std::string("Time of day: ") + time_of_day_labels[time_of_day(m)], "time-of-day", time_of_day_available(m, cb)); if (m.world.controlled_by_host) text(p.side, "time-status", server_can(m.multiplayer, "tod") ? "Your role sets it: time of day changes for the whole server." : "The lobby host controls world layers, including time of day."); load_status("load-status"); } else if (section == custom_section) { text(p.main, "custom-levels-title", "CUSTOM MAPS"); if (!destinations(true)) { text(p.main, "custom-levels-empty", "No custom maps installed."); text(p.main, "custom-levels-help", "Custom maps from the Mods folder appear here."); } text(p.side, "custom-title", "TRAVEL"); text(p.side, "custom-selected", selected && selected->custom ? level_name(*selected) : "Choose a map to load."); button(p.side, "custom-load-level", "Load selected map", "load-level", selected && selected->custom && can_load(s, m, cb), {}, true); load_status("custom-load-status"); } else if (section == parks_section) { text(p.main, "parks-title", "CHOOSE A PARK LOT"); for (unsigned i = 0; i < park_lots.size(); ++i) button(p.main, "park-lot-" + std::to_string(i), std::string(park_lots[i].label) + (s.lot == i ? " / SELECTED" : ""), "park-lot", true, std::string(park_lots[i].key)); text(p.main, "park-random-title", "ALL THREE PARK LOCATIONS"); button(p.main, "load-random-parks", "Load Random Parks", "load-random-parks", can_park(m, cb)); toggle(p.main, "park-random-on-launch", "Randomize on Launch", m.parks.randomize_on_launch, m.parks.available && cb.queue_console_command); text(p.side, "park-options", "PARK LAYOUT"); const bool available = can_park(m, cb); std::string family = "Empty lot"; for (unsigned i = 0; i < park_families.size(); ++i) if (s.parks[s.lot].starts_with(park_families[i])) family = park_family_labels[i]; button(p.side, "park-family", "Style: " + family, "park-family", available); button(p.side, "park-layout", park_label(s.parks[s.lot]), "park-layout", available); button(p.side, "load-park", "Load park layout", "load-park", available, {}, true); text(p.side, "park-status", m.world.map != WorldMap::bam ? "Load San Vansterdam to change parks." : parks_locked(m) ? "The lobby host controls park layouts." : m.parks.controlled_by_host ? "You are an admin: layouts you load change for the whole server." : ""); } else if (section == player_section) { text(p.main, "player-title", "SKATE YOUR WAY"); toggle(p.main, "noclip", "Noclip", m.debug.noclip, (m.debug.noclip_available || m.debug.noclip) && cb.queue_debug); toggle(p.main, "no-bail", "No bail", m.debug.no_bail, (m.debug.no_bail_available || m.debug.no_bail) && cb.queue_debug); toggle(p.main, "freecam", "Freecam", m.debug.free_camera, m.debug.available && m.debug.camera_available && cb.queue_debug); toggle(p.main, "hall-of-meat", "Hall of Meat", m.hall_of_meat.enabled, m.hall_of_meat.available && cb.queue_console_command); // (Not listed when its official mod is turned off in the launcher.) if (!m.road_rash.off) { toggle(p.main, "road-rash", "Road Rash", m.road_rash.enabled, m.road_rash.available && cb.queue_console_command); toggle(p.main, "road-rash-blood", "Road Rash blood", m.road_rash.blood, m.road_rash.available && m.road_rash.enabled && cb.queue_console_command); button(p.main, "road-rash-heal", "Heal Road Rash", "road-rash-heal", m.road_rash.available && m.road_rash.enabled && cb.queue_console_command); } if (!m.board_break.off) { toggle(p.main, "board-break", "Board Break", m.board_break.enabled, m.board_break.available && cb.queue_console_command); button(p.main, "board-break-deck", "Board Break deck: " + m.board_break.deck, "board-break-deck", m.board_break.available && m.board_break.has_deck && cb.queue_console_command); toggle(p.main, "board-break-always", "Board Break: always break", m.board_break.always, m.board_break.available && m.board_break.enabled && cb.queue_console_command); toggle(p.main, "board-break-wear", "Board Break: wear and tear", m.board_break.wear, m.board_break.available && m.board_break.enabled && cb.queue_console_command); } const auto& grind = m.grind_exit; const bool grind_console = grind.available && cb.queue_console_command; toggle(p.main, "grind-exits", "Grind exit triggers", grind.enabled, grind_console); button(p.main, "grind-exit-sides", std::string("Input Mode: ") + (grind.toe_heel ? "Toe / Heel" : "Left / Right"), "grind-exit-sides", grind_console); if (grind.toe_heel) button(p.main, "grind-exit-stance", std::string("Your stance: ") + (grind.stance == 2 ? "Goofy" : grind.stance == 1 ? "Regular" : "Not set"), "grind-exit-stance", grind_console); button(p.main, "grind-exit-strength", "Pop-out strength: " + std::to_string(grind.strength) + "%", "grind-exit-strength", grind_console); toggle(p.main, "board-wear", "Board wear", m.offline.board_wear.effective, m.offline.board_wear.available && cb.queue_offline_feature); button(p.main, "board-wear-reset", "Reset board wear", "board-wear-reset", m.offline.board_wear.available && m.offline.board_wear.effective && cb.queue_console_command); text(p.side, "flight-title", "FREE FLIGHT"); const auto speed = std::to_string(m.debug.camera_speed); button(p.side, "speed", "Flight speed: " + speed.substr(0, speed.find('.') + 2), "speed", (m.debug.free_camera || m.debug.noclip) && m.debug.camera_available && cb.queue_debug); text(p.side, "flight-help", "Close the menu to use flight controls."); // Local card only. The native menu reads this field under its model lock. text(p.side, "card-title", "PLAYER CARD NAME"); p.side.push_back(Row{"card-name", m.multiplayer.local_name.empty() ? "Your Steam name" : m.multiplayer.local_name, {}, m.player_card.custom_name, false, false, true}); button(p.side, "card-name-save", "Save card name", "card-name", m.player_card.available, {}, true); button(p.side, "card-name-reset", "Use Steam name", "card-name-reset", m.player_card.available && !m.player_card.custom_name.empty()); } else { text(p.main, "visuals-title", "CAMERA EFFECTS"); constexpr std::array names{"Film grain", "Vignette", "Chromatic aberration"}; for (unsigned i = 0; i < names.size(); ++i) { const auto choice = m.graphics.choices.effects[i]; const bool on = choice < 0 ? !m.graphics.ready[i] || m.graphics.enabled[i] : choice != 0; button(p.main, graphics_keys[i], std::string(names[i]) + (on ? ": On" : ": Off") + (choice < 0 ? " (Default)" : ""), "graphics", m.graphics.available && cb.queue_console_command, graphics_keys[i]); } button(p.main, "graphics-reset", "Restore default effects", "graphics-reset", m.graphics.available && cb.queue_console_command); text(p.main, "replay-title", "REPLAY EDITOR"); toggle(p.main, "keyframe-smoothing", "Keyframe smoothing", m.keyframe_smoothing.enabled, m.keyframe_smoothing.available && cb.queue_console_command); button(p.main, "keyframe-smoothness", "Keyframe smoothness: " + std::to_string(overlay::keyframe_smoothness_percent(m.keyframe_smoothing)) + "%", "keyframe-smoothness", m.keyframe_smoothing.available && m.keyframe_smoothing.enabled && cb.queue_console_command); text(p.side, "hud-title", "CHALLENGES"); button(p.side, "challenges", m.progression.challenges_hidden ? "Challenges: Hidden" : "Challenges: Shown", "challenges", m.progression.challenges_enabled && cb.queue_console_command); text(p.side, "challenges-help", "Showing or hiding keeps your progress."); } text(p.side, "tools-feedback-" + std::to_string(section), s.feedback); return p; } } // namespace dingosdk::native_tools