From a07102ba11eefd659485ad987afffa67d3dc7e0f Mon Sep 17 00:00:00 2001 From: iZePlayz <69536095+iZePlayzYT@users.noreply.github.com> Date: Thu, 11 Dec 2025 17:50:31 +0100 Subject: [PATCH] Added BungeeCord64 --- autogen/lua_definitions/functions.lua | 55 +++ mods/bungeecord64/main.lua | 434 ++++++++++++++++++ src/pc/lua/smlua_functions_autogen.c | 152 ++++++ src/pc/lua/utils/smlua_misc_utils.c | 56 +++ src/pc/lua/utils/smlua_misc_utils.h | 10 + src/pc/network/network.c | 190 +++++++- src/pc/network/network.h | 9 + src/pc/network/network_player.c | 31 +- src/pc/network/packets/packet.c | 3 + src/pc/network/packets/packet.h | 11 + .../network/packets/packet_bungee_fallback.c | 77 ++++ src/pc/network/packets/packet_join.c | 11 + start_bungeecord64_test.bat | 34 ++ 13 files changed, 1063 insertions(+), 10 deletions(-) create mode 100644 mods/bungeecord64/main.lua create mode 100644 src/pc/network/packets/packet_bungee_fallback.c create mode 100644 start_bungeecord64_test.bat diff --git a/autogen/lua_definitions/functions.lua b/autogen/lua_definitions/functions.lua index 676240c49..bb3ba0150 100644 --- a/autogen/lua_definitions/functions.lua +++ b/autogen/lua_definitions/functions.lua @@ -11176,6 +11176,61 @@ function set_got_file_coin_hi_score(value) -- ... end +--- @param port integer +--- @return boolean +--- [BungeeCord64] Switches to a different server on localhost with the specified port. Returns true if switch was initiated successfully. Only works when connected as client, not as server host. +function network_switch_to_server(port) + -- ... +end + +--- @return integer +--- [BungeeCord64] Gets the current server port the client is connected to or hosting on. Returns 0 if not connected. +function network_get_current_port() + -- ... +end + +--- @return string +--- [BungeeCord64] Gets the current connection IP address as a string. Returns empty string if not connected. +function network_get_current_ip() + -- ... +end + +--- @return boolean +--- [BungeeCord64] Checks if the player is currently connected to a server as a client. +function network_is_client() + -- ... +end + +--- @param port integer +--- [BungeeCord64] Sets the global fallback port used when the current server dies unexpectedly. +function network_set_bungee_fallback_port(port) + -- ... +end + +--- @return integer +--- [BungeeCord64] Gets the current global fallback port. +function network_get_bungee_fallback_port() + -- ... +end + +--- @return boolean +--- [BungeeCord64] Checks if a seamless server switch is currently in progress. +function network_is_bungee_switching() + -- ... +end + +--- @param port integer +--- [BungeeCord64] SERVER ONLY: Sets the fallback port that clients should reconnect to if this server crashes. +function network_set_server_fallback_port(port) + -- ... +end + +--- @return integer +--- [BungeeCord64] SERVER ONLY: Gets the configured fallback port for this server. +function network_get_server_fallback_port() + -- ... +end + --- @return boolean --- Checks if the save file has been modified without saving function get_save_file_modified() diff --git a/mods/bungeecord64/main.lua b/mods/bungeecord64/main.lua new file mode 100644 index 000000000..dc0c0d852 --- /dev/null +++ b/mods/bungeecord64/main.lua @@ -0,0 +1,434 @@ +-- name: BungeeCord64 +-- description:\#ffff33\--- BungeeCord64 v1.2 ---\n\n\#dcdcdc\Seamless multi-server switching for SM64CoopDX.\nLets you hop between local servers with a smooth transition overlay.\n\n\#ffff33\Commands:\n\#ffffff\/bungeecord64\#aaaaaa\ - Show status\n\#ffffff\/switch \#aaaaaa\ - Switch to server on port\n\#ffffff\/leave\#aaaaaa\ - Return to home server\n\#ffffff\/setfallback \#aaaaaa\ - Set default home server\n\#ffffff\/setserverfallback \#aaaaaa\ - (Server) Set fallback for clients\n\n\#00ff00\This mod must be installed on ALL servers!\n\#ff6666\All servers must be in the same local network (localhost) +-- incompatible: gamemode +-- pausable: false + +-- ===================================================== +-- BungeeCord64 - Seamless Server Switching System +-- ===================================================== +-- Enables seamless switching between multiple SM64CoopDX servers +-- in the local network (localhost). Inspired by Minecraft BungeeCord. +-- +-- Features: +-- - /switch : Smooth switch to another local server +-- - /leave : Returns to your home server +-- - Auto reconnect : Server sends fallback port to clients +-- - Overlay display : "Connecting To Server ..." while you can still play +-- ===================================================== + +if incompatibleClient then return end + +local MOD_VERSION = "1.2.0" + +-- Home server: where /leave goes to. +-- By default this is the first server you connect to as a client, +-- but it can be overridden with /setfallback. +local homePort = 0 + +-- Optional: configured fallback (if you want a fixed main hub) +-- Default: 7777, unless changed via /setfallback or saved config. +local fallbackPort = 7777 + +-- Known servers in the network (Port -> Name) for nicer status output +local knownServers = {} + +-- Runtime state +local lastWasConnected = false +local lastPort = 0 + +-- Note: Switch state is now managed C-side via network_is_bungee_switching() +-- We only track Lua-side state for UI/chat messages +local luaSwitchReason = nil + +-- Server-side: fallback port to send to clients +local serverFallbackPort = 0 + +-- =================== +-- Helper functions +-- =================== + +local function chatMsg(msg, color) + color = color or "\\#ffffff\\" + djui_chat_message_create(color .. msg) +end + +local function popup(msg, lines) + djui_popup_create(msg, lines or 2) +end + +local function isClient() + return network_is_client() +end + +local function isServer() + return network_is_server() +end + +local function getCurrentPort() + return network_get_current_port() or 0 +end + +local function getCurrentIp() + return network_get_current_ip() or "" +end + +local function ensureHomePort() + if homePort ~= 0 then return end + local port = getCurrentPort() + if port ~= 0 then + homePort = port + end +end + +local function getHomePort() + if homePort ~= 0 then return homePort end + if fallbackPort ~= 0 then return fallbackPort end + return 0 +end + +local function registerServer(port, name) + if not port or port <= 0 then return end + knownServers[port] = name or ("Server:" .. port) + mod_storage_save("server_" .. port, knownServers[port]) +end + +local function loadSavedConfig() + -- Load client fallback + local savedFallback = mod_storage_load("fallback_port") + if savedFallback and tonumber(savedFallback) then + fallbackPort = tonumber(savedFallback) + if homePort == 0 then + homePort = fallbackPort + end + end + + -- Push current fallback down into the C-side global so that even if Lua + -- dies (e.g. due to a hard disconnect), the client can still auto-reconnect + -- to the fallback server. + network_set_bungee_fallback_port(fallbackPort) + + -- Load server fallback (if hosting) + local savedServerFallback = mod_storage_load("server_fallback_port") + if savedServerFallback and tonumber(savedServerFallback) then + serverFallbackPort = tonumber(savedServerFallback) + end +end + +-- Core switching helper. Now uses the C-side seamless BungeeCord switch +-- which shows a big overlay while you can still play, then switches. +local function performSwitch(targetPort, reason) + if isServer() then + chatMsg("You cannot use BungeeCord64 while hosting.", "\\#ff6666\\") + return false + end + + local currentPort = getCurrentPort() + + if not isClient() and currentPort == 0 then + chatMsg("You are not connected to any server.", "\\#ff6666\\") + return false + end + + if targetPort == nil or targetPort <= 0 then + chatMsg("Invalid or unknown target port.", "\\#ff6666\\") + return false + end + + if currentPort ~= 0 and targetPort == currentPort then + chatMsg("You are already on port " .. targetPort .. ".", "\\#ffff00\\") + return false + end + + -- Check if a switch is already in progress (C-side check) + if network_is_bungee_switching() then + chatMsg("A server switch is already in progress.", "\\#ffff00\\") + return false + end + + ensureHomePort() + + local reasonText = reason or "switch" + chatMsg("BungeeCord64: Initiating switch to port " .. targetPort .. "...", "\\#00ff00\\") + + luaSwitchReason = reasonText + lastWasConnected = false + + -- Use the new C-side seamless switch + -- This shows the overlay immediately, lets you play for a moment, + -- then performs the actual switch + local ok = network_switch_to_server(targetPort) + if not ok then + chatMsg("BungeeCord64: failed to initiate switch to port " .. targetPort .. ".", "\\#ff6666\\") + luaSwitchReason = nil + return false + end + + return true +end + +-- =================== +-- Chat commands +-- =================== + +local function cmdSwitch(msg) + if msg == "" or msg == nil then + chatMsg("Usage: /switch ", "\\#ffff00\\") + chatMsg("Example: /switch 7778", "\\#aaaaaa\\") + return true + end + + local port = tonumber(msg) + if not port then + chatMsg("Invalid port: " .. msg, "\\#ff6666\\") + return true + end + + registerServer(port, knownServers[port]) + performSwitch(port, "manual") + return true +end + +local function cmdLeave(msg) + local target = getHomePort() + if target == 0 then + chatMsg("No home server known yet. Join a server first or use /setfallback .", "\\#ff6666\\") + return true + end + + performSwitch(target, "leave") + return true +end + +local function cmdSetFallback(msg) + if msg == "" or msg == nil then + chatMsg("Usage: /setfallback ", "\\#ffff00\\") + local currentHome = getHomePort() + if currentHome ~= 0 then + chatMsg("Current home server: port " .. currentHome, "\\#aaaaaa\\") + else + chatMsg("No home server configured.", "\\#aaaaaa\\") + end + return true + end + + local port = tonumber(msg) + if not port or port <= 0 then + chatMsg("Invalid port: " .. msg, "\\#ff6666\\") + return true + end + + fallbackPort = port + mod_storage_save("fallback_port", tostring(port)) + if homePort == 0 then + homePort = port + end + + -- Update C-side fallback so C can auto-reconnect on hard disconnects. + network_set_bungee_fallback_port(fallbackPort) + + chatMsg("Fallback (home) server set to port " .. port .. ".", "\\#00ff00\\") + popup("BungeeCord64\nHome server: port " .. port, 2) + return true +end + +-- Server-only command: set fallback port that gets sent to clients +local function cmdSetServerFallback(msg) + if not isServer() then + chatMsg("This command is only for server hosts.", "\\#ff6666\\") + return true + end + + if msg == "" or msg == nil then + chatMsg("Usage: /setserverfallback ", "\\#ffff00\\") + local current = network_get_server_fallback_port() + if current ~= 0 then + chatMsg("Current server fallback: port " .. current, "\\#aaaaaa\\") + else + chatMsg("No server fallback configured.", "\\#aaaaaa\\") + end + return true + end + + local port = tonumber(msg) + if not port or port <= 0 then + chatMsg("Invalid port: " .. msg, "\\#ff6666\\") + return true + end + + serverFallbackPort = port + mod_storage_save("server_fallback_port", tostring(port)) + + -- Set C-side server fallback + network_set_server_fallback_port(port) + + chatMsg("Server fallback port set to " .. port .. ".", "\\#00ff00\\") + chatMsg("Clients will reconnect to this port if this server crashes.", "\\#aaaaaa\\") + popup("BungeeCord64\nServer fallback: port " .. port, 2) + return true +end + +local function cmdAddServer(msg) + if msg == "" or msg == nil then + chatMsg("Usage: /addserver ", "\\#ffff00\\") + chatMsg("Example: /addserver 7779 Minigames", "\\#aaaaaa\\") + return true + end + + local parts = {} + for part in msg:gmatch("%S+") do + table.insert(parts, part) + end + + local port = tonumber(parts[1]) + if not port or port <= 0 then + chatMsg("Invalid port!", "\\#ff6666\\") + return true + end + + local name = "Server:" .. port + if #parts > 1 then + table.remove(parts, 1) + name = table.concat(parts, " ") + end + + registerServer(port, name) + chatMsg("Registered server: " .. name .. " (port " .. port .. ")", "\\#00ff00\\") + return true +end + +local function cmdStatus(msg) + local client = isClient() + local server = isServer() + local port = getCurrentPort() + local ip = getCurrentIp() + + chatMsg("============================================", "\\#ffff33\\") + chatMsg(" BungeeCord64 v" .. MOD_VERSION, "\\#ffff33\\") + chatMsg("============================================", "\\#ffff33\\") + + if server then + chatMsg(">> You are HOSTING a server on port " .. port, "\\#00ff00\\") + local srvFallback = network_get_server_fallback_port() + if srvFallback ~= 0 then + chatMsg(" Server fallback port: " .. srvFallback, "\\#00ffff\\") + else + chatMsg(" Server fallback: (not set)", "\\#aaaaaa\\") + chatMsg(" Use /setserverfallback to set one.", "\\#aaaaaa\\") + end + elseif client and port ~= 0 then + chatMsg(">> Connected as CLIENT on port " .. port, "\\#00ff00\\") + if ip ~= "" then + chatMsg(" Server IP: " .. ip, "\\#aaaaaa\\") + end + + -- Show the fallback port the server sent us + local receivedFallback = network_get_bungee_fallback_port() + if receivedFallback ~= 0 then + chatMsg(" Server's fallback port: " .. receivedFallback, "\\#00ffff\\") + end + else + chatMsg(">> Not connected to any server.", "\\#ff6666\\") + end + + local home = getHomePort() + if home ~= 0 then + chatMsg("Home server (target for /leave): port " .. home, "\\#ffff00\\") + else + chatMsg("Home server (target for /leave): (not set)", "\\#aaaaaa\\") + end + + chatMsg("Client fallback port: " .. tostring(fallbackPort), "\\#00ffff\\") + + chatMsg("", "\\#ffffff\\") + chatMsg(">> Commands:", "\\#00ffff\\") + chatMsg("/switch - Switch to server", "\\#aaaaaa\\") + chatMsg("/leave - Return to home server", "\\#aaaaaa\\") + chatMsg("/setfallback - Set client home", "\\#aaaaaa\\") + if server then + chatMsg("/setserverfallback - Set server fallback", "\\#aaaaaa\\") + end + chatMsg("/addserver - Register server", "\\#aaaaaa\\") + chatMsg("============================================", "\\#ffff33\\") + + return true +end + +-- =================== +-- Server init: set fallback port on startup +-- =================== + +local function onServerInit() + if not isServer() then return end + + -- Load saved server fallback + local savedFallback = mod_storage_load("server_fallback_port") + if savedFallback and tonumber(savedFallback) then + serverFallbackPort = tonumber(savedFallback) + network_set_server_fallback_port(serverFallbackPort) + chatMsg("BungeeCord64: Server fallback port loaded: " .. serverFallbackPort, "\\#00ffff\\") + end +end + +-- =================== +-- Update hook (connection tracking) +-- =================== + +local function onUpdate() + local client = isClient() + local server = isServer() + local port = getCurrentPort() + local switching = network_is_bungee_switching() + + if client and port ~= 0 then + -- Update home port the first time we see a valid connection + ensureHomePort() + + -- Check if we just completed a switch + if luaSwitchReason ~= nil and not switching then + chatMsg("BungeeCord64: Connected to port " .. port .. "!", "\\#00ff00\\") + luaSwitchReason = nil + end + + lastWasConnected = true + lastPort = port + return + end + + -- If we were connected as client and now we are not, and this wasn't + -- an intentional switch that is still in progress, the C-side will + -- handle auto-reconnect to fallback port. + if lastWasConnected and not switching then + lastWasConnected = false + end + + lastWasConnected = client and port ~= 0 +end + +-- =================== +-- Init & hook registration +-- =================== + +local function init() + loadSavedConfig() + + chatMsg("============================================", "\\#ffff33\\") + chatMsg("BungeeCord64 v" .. MOD_VERSION .. " loaded!", "\\#00ff00\\") + chatMsg("Use /bungeecord64 for status and help.", "\\#aaaaaa\\") + chatMsg("============================================", "\\#ffff33\\") + + -- If we're a server, set up server fallback + if isServer() then + onServerInit() + end +end + +hook_event(HOOK_UPDATE, onUpdate) + +hook_chat_command("bungeecord64", "[BC64] Show status and help", cmdStatus) +hook_chat_command("switch", "[BC64] /switch - Switch to server", cmdSwitch) +hook_chat_command("leave", "[BC64] /leave - Return to home server", cmdLeave) +hook_chat_command("setfallback", "[BC64] /setfallback - Set home server", cmdSetFallback) +hook_chat_command("setserverfallback", "[BC64] /setserverfallback - Set server fallback (host only)", cmdSetServerFallback) +hook_chat_command("addserver", "[BC64] /addserver - Register server", cmdAddServer) + +init() \ No newline at end of file diff --git a/src/pc/lua/smlua_functions_autogen.c b/src/pc/lua/smlua_functions_autogen.c index 3a08fe7b7..7f60db72c 100644 --- a/src/pc/lua/smlua_functions_autogen.c +++ b/src/pc/lua/smlua_functions_autogen.c @@ -33550,6 +33550,149 @@ int smlua_func_set_got_file_coin_hi_score(lua_State* L) { return 1; } +// BungeeCord64 Network Functions + +int smlua_func_network_switch_to_server(lua_State* L) { + if (L == NULL) { return 0; } + + int top = lua_gettop(L); + if (top != 1) { + LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "network_switch_to_server", 1, top); + return 0; + } + + u32 port = smlua_to_integer(L, 1); + if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter 1 for function '%s'", "network_switch_to_server"); return 0; } + + lua_pushboolean(L, network_switch_to_server(port)); + + return 1; +} + +int smlua_func_network_get_current_port(UNUSED lua_State* L) { + if (L == NULL) { return 0; } + + int top = lua_gettop(L); + if (top != 0) { + LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "network_get_current_port", 0, top); + return 0; + } + + lua_pushinteger(L, network_get_current_port()); + + return 1; +} + +int smlua_func_network_get_current_ip(UNUSED lua_State* L) { + if (L == NULL) { return 0; } + + int top = lua_gettop(L); + if (top != 0) { + LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "network_get_current_ip", 0, top); + return 0; + } + + lua_pushstring(L, network_get_current_ip()); + + return 1; +} + +int smlua_func_network_is_client(UNUSED lua_State* L) { + if (L == NULL) { return 0; } + + int top = lua_gettop(L); + if (top != 0) { + LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "network_is_client", 0, top); + return 0; + } + + lua_pushboolean(L, network_is_client()); + + return 1; +} + +int smlua_func_network_set_bungee_fallback_port(lua_State* L) { + if (L == NULL) { return 0; } + + int top = lua_gettop(L); + if (top != 1) { + LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "network_set_bungee_fallback_port", 1, top); + return 0; + } + + u32 port = smlua_to_integer(L, 1); + if (!gSmLuaConvertSuccess) { + LOG_LUA("Failed to convert parameter 1 for function '%s'", "network_set_bungee_fallback_port"); + return 0; + } + + network_set_bungee_fallback_port(port); + + return 0; +} + +int smlua_func_network_get_bungee_fallback_port(UNUSED lua_State* L) { + if (L == NULL) { return 0; } + + int top = lua_gettop(L); + if (top != 0) { + LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "network_get_bungee_fallback_port", 0, top); + return 0; + } + + lua_pushinteger(L, network_get_bungee_fallback_port()); + + return 1; +} + +int smlua_func_network_is_bungee_switching(UNUSED lua_State* L) { + if (L == NULL) { return 0; } + + int top = lua_gettop(L); + if (top != 0) { + LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "network_is_bungee_switching", 0, top); + return 0; + } + + lua_pushboolean(L, network_is_bungee_switching()); + + return 1; +} + +int smlua_func_network_set_server_fallback_port(lua_State* L) { + if (L == NULL) { return 0; } + + int top = lua_gettop(L); + if (top != 1) { + LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "network_set_server_fallback_port", 1, top); + return 0; + } + + u32 port = smlua_to_integer(L, 1); + if (!gSmLuaConvertSuccess) { + LOG_LUA("Failed to convert parameter 1 for function '%s'", "network_set_server_fallback_port"); + return 0; + } + + network_set_server_fallback_port(port); + + return 0; +} + +int smlua_func_network_get_server_fallback_port(UNUSED lua_State* L) { + if (L == NULL) { return 0; } + + int top = lua_gettop(L); + if (top != 0) { + LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "network_get_server_fallback_port", 0, top); + return 0; + } + + lua_pushinteger(L, network_get_server_fallback_port()); + + return 1; +} + int smlua_func_get_save_file_modified(UNUSED lua_State* L) { if (L == NULL) { return 0; } @@ -38756,6 +38899,15 @@ void smlua_bind_functions_autogen(void) { smlua_bind_function(L, "set_last_completed_star_num", smlua_func_set_last_completed_star_num); smlua_bind_function(L, "get_got_file_coin_hi_score", smlua_func_get_got_file_coin_hi_score); smlua_bind_function(L, "set_got_file_coin_hi_score", smlua_func_set_got_file_coin_hi_score); + smlua_bind_function(L, "network_switch_to_server", smlua_func_network_switch_to_server); + smlua_bind_function(L, "network_get_current_port", smlua_func_network_get_current_port); + smlua_bind_function(L, "network_get_current_ip", smlua_func_network_get_current_ip); + smlua_bind_function(L, "network_is_client", smlua_func_network_is_client); + smlua_bind_function(L, "network_set_bungee_fallback_port", smlua_func_network_set_bungee_fallback_port); + smlua_bind_function(L, "network_get_bungee_fallback_port", smlua_func_network_get_bungee_fallback_port); + smlua_bind_function(L, "network_is_bungee_switching", smlua_func_network_is_bungee_switching); + smlua_bind_function(L, "network_set_server_fallback_port", smlua_func_network_set_server_fallback_port); + smlua_bind_function(L, "network_get_server_fallback_port", smlua_func_network_get_server_fallback_port); smlua_bind_function(L, "get_save_file_modified", smlua_func_get_save_file_modified); smlua_bind_function(L, "set_save_file_modified", smlua_func_set_save_file_modified); smlua_bind_function(L, "hud_hide", smlua_func_hud_hide); diff --git a/src/pc/lua/utils/smlua_misc_utils.c b/src/pc/lua/utils/smlua_misc_utils.c index 75cdd6094..a02b8fd68 100644 --- a/src/pc/lua/utils/smlua_misc_utils.c +++ b/src/pc/lua/utils/smlua_misc_utils.c @@ -16,6 +16,8 @@ #include "pc/mods/mods.h" #include "pc/mods/mods_utils.h" #include "pc/pc_main.h" +#include "pc/network/network.h" +#include "pc/configfile.h" #include "game/object_list_processor.h" #include "game/rendering_graph_node.h" #include "game/level_update.h" @@ -623,6 +625,60 @@ const char* get_os_name(void) { } /// +// BungeeCord64 Network Switching Functions +// Enables switching between multiple SM64CoopDX servers in local network +/// + +bool network_switch_to_server(u32 port) { + // Only allow switching when acting as a client. + if (gNetworkType != NT_CLIENT) { + LOG_INFO("BungeeCord64: Cannot switch server - not connected as client"); + return false; + } + + if (port == 0) { + LOG_INFO("BungeeCord64: Cannot switch server - invalid port 0"); + return false; + } + + // Check if already reconnecting + if (network_is_reconnecting()) { + LOG_INFO("BungeeCord64: Reconnect already in progress"); + return false; + } + + LOG_INFO("BungeeCord64: Switching to server on port %u", port); + + // Use the standard reconnect flow with the normal connect screen + network_bungee_switch_begin(port); + + return true; +} + +u32 network_get_current_port(void) { + if (gNetworkType == NT_CLIENT) { + return configJoinPort; + } else if (gNetworkType == NT_SERVER) { + return configHostPort; + } + return 0; +} + +const char* network_get_current_ip(void) { + static char currentIp[MAX_CONFIG_STRING] = ""; + if (gNetworkType == NT_CLIENT) { + snprintf(currentIp, MAX_CONFIG_STRING, "%s", configJoinIp); + return currentIp; + } else if (gNetworkType == NT_SERVER) { + snprintf(currentIp, MAX_CONFIG_STRING, "localhost"); + return currentIp; + } + return ""; +} + +bool network_is_client(void) { + return gNetworkType == NT_CLIENT; +} struct GraphNodeRoot* geo_get_current_root(void) { return gCurGraphNodeRoot; diff --git a/src/pc/lua/utils/smlua_misc_utils.h b/src/pc/lua/utils/smlua_misc_utils.h index b9413649a..b4feeb21b 100644 --- a/src/pc/lua/utils/smlua_misc_utils.h +++ b/src/pc/lua/utils/smlua_misc_utils.h @@ -248,6 +248,16 @@ void reset_window_title(void); /* |description|Gets the name of the operating system the game is running on|descriptionEnd| */ const char* get_os_name(void); +// BungeeCord64 - Network Switching Functions for local server hopping +/* |description|[BungeeCord64] Switches to a different server on localhost with the specified port. Returns true if switch was initiated successfully. Only works when connected as client, not as server host|descriptionEnd| */ +bool network_switch_to_server(u32 port); +/* |description|[BungeeCord64] Gets the current server port the client is connected to or hosting on. Returns 0 if not connected|descriptionEnd| */ +u32 network_get_current_port(void); +/* |description|[BungeeCord64] Gets the current connection IP address as a string. Returns empty string if not connected|descriptionEnd| */ +const char* network_get_current_ip(void); +/* |description|[BungeeCord64] Checks if the player is currently connected to a server as a client|descriptionEnd| */ +bool network_is_client(void); + /* |description|Gets the current GraphNodeRoot|descriptionEnd|*/ struct GraphNodeRoot* geo_get_current_root(void); diff --git a/src/pc/network/network.c b/src/pc/network/network.c index d267d7cb8..7369e98e2 100644 --- a/src/pc/network/network.c +++ b/src/pc/network/network.c @@ -73,6 +73,16 @@ u32 gNetworkStartupTimer = 0; u32 sNetworkReconnectTimer = 0; u32 sNetworkRehostTimer = 0; enum NetworkSystemType sNetworkReconnectType = NS_SOCKET; +static u32 sBungeeFallbackPort = 0; + +// BungeeCord64: Timer for connection timeout during switch +// If connection isn't established within this time, try fallback port +static u32 sBungeeConnectionTimer = 0; +static u32 sBungeeTargetPort = 0; +static u32 sBungeePendingSwitchPort = 0; // Port to switch to (delayed execution) +static u32 sBungeePreviousFallbackPort = 0; // Fallback port before switch (in case new server doesn't send one) +static u32 sBungeeFirstServerPort = 0; // The first server port we connected to (used as ultimate fallback) +#define BUNGEE_CONNECTION_TIMEOUT (15 * 30) // 15 seconds struct ServerSettings gServerSettings = { .playerInteractions = PLAYER_INTERACTIONS_SOLID, @@ -453,6 +463,30 @@ void network_reset_reconnect_and_rehost(void) { sNetworkReconnectType = NS_SOCKET; } +u32 network_get_bungee_fallback_port(void) { + // Return current fallback port, or previous one if current is not set + // Fall back to first server port as ultimate fallback + if (sBungeeFallbackPort != 0) { + return sBungeeFallbackPort; + } + if (sBungeePreviousFallbackPort != 0) { + return sBungeePreviousFallbackPort; + } + return sBungeeFirstServerPort; +} + +void network_set_bungee_first_server_port(u32 port) { + // Only set if not already set (first connection) + if (sBungeeFirstServerPort == 0 && port != 0) { + sBungeeFirstServerPort = port; + LOG_INFO("BungeeCord64: First server port set to %u", port); + } +} + +void network_set_bungee_fallback_port(u32 port) { + sBungeeFallbackPort = port; +} + void network_reconnect_begin(void) { if (sNetworkReconnectTimer > 0) { return; @@ -492,6 +526,136 @@ bool network_is_reconnecting(void) { return sNetworkReconnectTimer > 0; } + +// ===================================================== +// BungeeCord64 Simple Server Switch +// ===================================================== +// Uses the standard reconnect flow with the default connect screen. +// If the target server is offline, automatically tries the fallback port. +// Switch is delayed by 1 frame to avoid crashes when called from Lua callbacks. + +void network_bungee_switch_begin(u32 targetPort) { + if (targetPort == 0) { + LOG_ERROR("BungeeCord64: Invalid target port 0"); + return; + } + + if (sNetworkReconnectTimer > 0 || sBungeePendingSwitchPort != 0) { + LOG_INFO("BungeeCord64: Switch already in progress"); + return; + } + + LOG_INFO("BungeeCord64: Scheduling switch to port %u", targetPort); + + // Schedule the switch for next frame (avoids crash when called from Lua callback) + sBungeePendingSwitchPort = targetPort; +} + +// Actually performs the switch - called from network_update +static void network_bungee_execute_pending_switch(void) { + if (sBungeePendingSwitchPort == 0) { return; } + + u32 targetPort = sBungeePendingSwitchPort; + sBungeePendingSwitchPort = 0; + + LOG_INFO("BungeeCord64: Executing switch to port %u", targetPort); + + // Save current fallback port before switch (in case new server doesn't send one) + if (sBungeeFallbackPort != 0) { + sBungeePreviousFallbackPort = sBungeeFallbackPort; + LOG_INFO("BungeeCord64: Saved previous fallback port %u", sBungeePreviousFallbackPort); + } + + // Save current port as first server port if not already set + // This ensures we have a fallback to the original server + if (sBungeeFirstServerPort == 0) { + sBungeeFirstServerPort = configJoinPort; + LOG_INFO("BungeeCord64: Saved first server port %u", sBungeeFirstServerPort); + } + + // Save target port and start connection timer + sBungeeTargetPort = targetPort; + sBungeeConnectionTimer = BUNGEE_CONNECTION_TIMEOUT; + + // Update config for new connection (localhost only for BungeeCord) + snprintf(configJoinIp, MAX_CONFIG_STRING, "127.0.0.1"); + configJoinPort = targetPort; + + // Set up reconnect timer + sNetworkReconnectTimer = 2 * 30; + sNetworkReconnectType = NS_SOCKET; + + // IMPORTANT: Send leave packet so old server knows we're leaving + // and use reconnecting=false so mods get properly unloaded + network_shutdown(true, false, false, false); // sendLeaving=true, reconnecting=false + + // Open connect menu + djui_connect_menu_open(); +} + +// Called from network_update to check for connection timeout +static void network_bungee_connection_timeout_update(void) { + if (sBungeeConnectionTimer == 0) { return; } + + // If we're connected, cancel the timer + if (gNetworkType == NT_CLIENT && gNetworkSentJoin) { + LOG_INFO("BungeeCord64: Connection established, canceling timeout"); + sBungeeConnectionTimer = 0; + sBungeeTargetPort = 0; + return; + } + + // Countdown + sBungeeConnectionTimer--; + + // If timer expired and we're still not connected, try fallback + if (sBungeeConnectionTimer == 0) { + u32 fbPort = network_get_bungee_fallback_port(); + + // Only try fallback if it's different from what we tried + if (fbPort != 0 && fbPort != sBungeeTargetPort) { + LOG_INFO("BungeeCord64: Connection to port %u timed out, trying fallback port %u", + sBungeeTargetPort, fbPort); + + sBungeeTargetPort = 0; + + // Update config for fallback connection + snprintf(configJoinIp, MAX_CONFIG_STRING, "127.0.0.1"); + configJoinPort = fbPort; + + // Restart reconnect to fallback + network_reconnect_begin(); + } else { + LOG_INFO("BungeeCord64: Connection timed out, no fallback available"); + sBungeeTargetPort = 0; + } + } +} + +void network_bungee_switch_complete(void) { + // Cancel connection timer on successful connection + sBungeeConnectionTimer = 0; + sBungeeTargetPort = 0; +} + +bool network_is_bungee_switching(void) { + // Check if we're reconnecting, waiting for connection, or have a pending switch + return sNetworkReconnectTimer > 0 || sBungeeConnectionTimer > 0 || sBungeePendingSwitchPort != 0; +} + +u8 network_get_bungee_switch_phase(void) { + // Return 0 (no custom phases anymore) + return 0; +} + +u32 network_get_bungee_switch_target(void) { + // Return the target port we're trying to connect to + if (sBungeeTargetPort != 0) { + return sBungeeTargetPort; + } + return configJoinPort; +} + void network_rehost_begin(void) { for (int i = 1; i < MAX_PLAYERS; i++) { struct NetworkPlayer* np = &gNetworkPlayers[i]; @@ -562,8 +726,12 @@ void network_update(void) { gNetworkStartupTimer--; } + // Execute pending BungeeCord switch (delayed to avoid Lua callback crash) + network_bungee_execute_pending_switch(); + network_rehost_update(); network_reconnect_update(); + network_bungee_connection_timeout_update(); #ifdef COOPNET network_update_coopnet(); @@ -701,6 +869,21 @@ void network_shutdown(bool sendLeaving, bool exiting, bool popup, bool reconnect dynos_model_clear_pool(MODEL_POOL_SESSION); + // When reconnecting, keep Lua and mods alive so that calls originating + // from Lua (e.g. BungeeCord64) do not destroy the VM mid-execution. + // We still fully reset the graphics/game state below. + if (!reconnecting) { + camera_reset_overrides(); + romhack_camera_reset_settings(); + free_vtx_scroll_targets(); + dynos_mod_shutdown(); + mods_clear(&gActiveMods); + mods_clear(&gRemoteMods); + smlua_shutdown(); + } else { + free_vtx_scroll_targets(); + } + // reset other stuff extern u8* gOverrideEeprom; gOverrideEeprom = NULL; @@ -726,13 +909,6 @@ void network_shutdown(bool sendLeaving, bool exiting, bool popup, bool reconnect gRomhackCameraSettings.centering = FALSE; gOverrideAllowToxicGasCamera = FALSE; gRomhackCameraSettings.dpad = FALSE; - camera_reset_overrides(); - romhack_camera_reset_settings(); - free_vtx_scroll_targets(); - dynos_mod_shutdown(); - mods_clear(&gActiveMods); - mods_clear(&gRemoteMods); - smlua_shutdown(); extern s16 gChangeLevel; gChangeLevel = LEVEL_CASTLE_GROUNDS; network_player_init(); diff --git a/src/pc/network/network.h b/src/pc/network/network.h index 1ba8e6320..8bef653d8 100644 --- a/src/pc/network/network.h +++ b/src/pc/network/network.h @@ -130,5 +130,14 @@ bool network_allow_mod_dev_mode(void); void network_mod_dev_mode_reload(void); void network_update(void); void network_shutdown(bool sendLeaving, bool exiting, bool popup, bool reconnecting); +u32 network_get_bungee_fallback_port(void); +void network_set_bungee_fallback_port(u32 port); +void network_set_bungee_first_server_port(u32 port); + +// BungeeCord64 simple server switching (uses standard reconnect) +void network_bungee_switch_begin(u32 targetPort); +void network_bungee_switch_complete(void); +bool network_is_bungee_switching(void); +u8 network_get_bungee_switch_phase(void); #endif diff --git a/src/pc/network/network_player.c b/src/pc/network/network_player.c index 5e1661bbb..9086c985a 100644 --- a/src/pc/network/network_player.c +++ b/src/pc/network/network_player.c @@ -5,6 +5,7 @@ #include "pc/djui/djui.h" #include "pc/debuglog.h" #include "pc/utils/misc.h" +#include "pc/configfile.h" #include "game/area.h" #include "game/level_info.h" #include "game/hardcoded.h" @@ -243,8 +244,22 @@ void network_player_update(void) { #else if (elapsed > NETWORK_PLAYER_TIMEOUT * 1.5f) { #endif - LOG_INFO("dropping due to no server connectivity"); - network_shutdown(false, false, true, false); + // Don't trigger disconnect handling if we're in the middle of a BungeeCord switch + if (network_is_bungee_switching()) { + return; + } + + u32 fbPort = network_get_bungee_fallback_port(); + LOG_INFO("BungeeCord64: Server timeout - fallback port: %u, current port: %u", fbPort, configJoinPort); + + if (fbPort != 0 && fbPort != configJoinPort) { + LOG_INFO("BungeeCord64: Auto-reconnecting to fallback port %u", fbPort); + // Use BungeeCord switch mechanism for proper fallback handling + network_bungee_switch_begin(fbPort); + } else { + LOG_INFO("dropping due to no server connectivity (no fallback configured)"); + network_shutdown(false, false, true, false); + } } elapsed = (clock_elapsed() - np->lastSent); @@ -373,7 +388,17 @@ u8 network_player_disconnected(u8 globalIndex) { LOG_ERROR("player disconnected, but it's local.. this shouldn't happen!"); return UNKNOWN_GLOBAL_INDEX; } else { - network_shutdown(true, false, true, false); + // BungeeCord64: Try to fallback to another server instead of just disconnecting + u32 fbPort = network_get_bungee_fallback_port(); + LOG_INFO("BungeeCord64: Server disconnected - fallback port: %u, current port: %u", fbPort, configJoinPort); + + if (fbPort != 0 && fbPort != configJoinPort) { + LOG_INFO("BungeeCord64: Auto-reconnecting to fallback port %u", fbPort); + network_bungee_switch_begin(fbPort); + } else { + network_shutdown(true, false, true, false); + } + return UNKNOWN_GLOBAL_INDEX; } } diff --git a/src/pc/network/packets/packet.c b/src/pc/network/packets/packet.c index 199a88e5a..4fac422b0 100644 --- a/src/pc/network/packets/packet.c +++ b/src/pc/network/packets/packet.c @@ -139,6 +139,9 @@ void packet_process(struct Packet* p) { case PACKET_LUA_CUSTOM: network_receive_lua_custom(p); break; case PACKET_LUA_CUSTOM_BYTESTRING: network_receive_lua_custom_bytestring(p); break; + // BungeeCord64 + case PACKET_BUNGEE_FALLBACK: network_receive_bungee_fallback(p); break; + // custom case PACKET_CUSTOM: network_receive_custom(p); break; default: LOG_ERROR("received unknown packet: %d", p->buffer[0]); diff --git a/src/pc/network/packets/packet.h b/src/pc/network/packets/packet.h index 5ab859760..574797913 100644 --- a/src/pc/network/packets/packet.h +++ b/src/pc/network/packets/packet.h @@ -77,6 +77,9 @@ enum PacketType { PACKET_COMMAND, PACKET_MODERATOR, + + // BungeeCord64 - Server sends fallback port to client + PACKET_BUNGEE_FALLBACK, /// PACKET_CUSTOM = 255, @@ -384,4 +387,12 @@ void network_receive_lua_custom(struct Packet* p); void network_send_lua_custom_bytestring(bool broadcast); void network_receive_lua_custom_bytestring(struct Packet* p); +// packet_bungee_fallback.c +void network_set_server_fallback_port(u32 port); +u32 network_get_server_fallback_port(void); +void network_send_bungee_fallback(u8 toLocalIndex, u32 fallbackPort); +void network_send_bungee_fallback_request(void); +void network_receive_bungee_fallback(struct Packet* p); +void network_receive_bungee_fallback_request(struct Packet* p); + #endif diff --git a/src/pc/network/packets/packet_bungee_fallback.c b/src/pc/network/packets/packet_bungee_fallback.c new file mode 100644 index 000000000..822f89e5c --- /dev/null +++ b/src/pc/network/packets/packet_bungee_fallback.c @@ -0,0 +1,77 @@ +// BungeeCord64 - Fallback Port Packet +// Server sends its fallback port to clients so they know where to reconnect +// if the server crashes unexpectedly. + +#include +#include "../network.h" +#include "pc/debuglog.h" +#include "pc/configfile.h" + +// Server-side: configured fallback port (where clients should go if this server dies) +// This can be set via server config or command +static u32 sServerFallbackPort = 0; + +void network_set_server_fallback_port(u32 port) { + sServerFallbackPort = port; + LOG_INFO("BungeeCord64: Server fallback port set to %u", port); +} + +u32 network_get_server_fallback_port(void) { + return sServerFallbackPort; +} + +// Server sends fallback port to a specific client +void network_send_bungee_fallback(u8 toLocalIndex, u32 fallbackPort) { + if (gNetworkType != NT_SERVER) { return; } + + struct Packet p = { 0 }; + packet_init(&p, PACKET_BUNGEE_FALLBACK, true, PLMT_NONE); + packet_write(&p, &fallbackPort, sizeof(u32)); + + network_send_to(toLocalIndex, &p); + LOG_INFO("BungeeCord64: Sent fallback port %u to player %d", fallbackPort, toLocalIndex); +} + +// Client requests fallback port from server +void network_send_bungee_fallback_request(void) { + if (gNetworkType != NT_CLIENT) { return; } + + struct Packet p = { 0 }; + packet_init(&p, PACKET_BUNGEE_FALLBACK, true, PLMT_NONE); + + // Empty packet = request + u32 zero = 0; + packet_write(&p, &zero, sizeof(u32)); + + network_send_to(PACKET_DESTINATION_SERVER, &p); + LOG_INFO("BungeeCord64: Requesting fallback port from server"); +} + +// Server receives request, Client receives fallback port +void network_receive_bungee_fallback(struct Packet* p) { + u32 port = 0; + packet_read(p, &port, sizeof(u32)); + + if (gNetworkType == NT_SERVER) { + // This is a request from a client + if (sServerFallbackPort != 0) { + network_send_bungee_fallback(p->localIndex, sServerFallbackPort); + } else { + LOG_INFO("BungeeCord64: Client requested fallback port, but none configured on this server"); + } + } else if (gNetworkType == NT_CLIENT) { + // This is the server sending us the fallback port + if (port != 0) { + network_set_bungee_fallback_port(port); + LOG_INFO("BungeeCord64: Received fallback port %u from server", port); + } else { + LOG_INFO("BungeeCord64: Server has no fallback port configured, keeping previous: %u", + network_get_bungee_fallback_port()); + } + } +} + +// Alias for backwards compatibility +void network_receive_bungee_fallback_request(struct Packet* p) { + network_receive_bungee_fallback(p); +} diff --git a/src/pc/network/packets/packet_join.c b/src/pc/network/packets/packet_join.c index 0f1602352..35d2d389b 100644 --- a/src/pc/network/packets/packet_join.c +++ b/src/pc/network/packets/packet_join.c @@ -200,7 +200,18 @@ void network_receive_join(struct Packet* p) { network_send_network_players_request(); network_send_lua_sync_table_request(); + // BungeeCord64: Save the first server port as ultimate fallback + // This ensures we always have a fallback even if no server configures one + network_set_bungee_first_server_port(configJoinPort); + + // BungeeCord64: Request fallback port from server + network_send_bungee_fallback_request(); + gCurrentlyJoining = false; + + // Complete BungeeCord switch if one was in progress + network_bungee_switch_complete(); + smlua_call_event_hooks(HOOK_JOINED_GAME); extern s16 gChangeLevel; gChangeLevel = gLevelValues.entryLevel; diff --git a/start_bungeecord64_test.bat b/start_bungeecord64_test.bat new file mode 100644 index 000000000..8a8caaaa5 --- /dev/null +++ b/start_bungeecord64_test.bat @@ -0,0 +1,34 @@ +@echo off +REM ===================================================== +REM BungeeCord64 Test Environment Launcher +REM ===================================================== +REM This script launches 4 instances of SM64CoopDX: +REM - Server 1 on port 7777 (Main Server) +REM - Server 2 on port 7778 +REM - Server 3 on port 7779 +REM - Player/Client instance (starts normally, join via menu) +REM ===================================================== + +REM Set the path to the executable (adjust if needed) +SET GAME_EXE=build\us_pc\sm64coopdx.exe + +REM Check if executable exists +if not exist "%GAME_EXE%" ( + echo ERROR: Game executable not found at %GAME_EXE% + pause + exit /b 1 +) + +REM Start all servers and player without showing console windows +start "" /min "%GAME_EXE%" --server 7777 --skip-intro --configfile config_server_7777.txt +timeout /t 2 /nobreak > nul + +start "" /min "%GAME_EXE%" --server 7778 --skip-intro --configfile config_server_7778.txt +timeout /t 2 /nobreak > nul + +start "" /min "%GAME_EXE%" --server 7779 --skip-intro --configfile config_server_7779.txt +timeout /t 2 /nobreak > nul + +start "" "%GAME_EXE%" --configfile config_player.txt + +exit