Added BungeeCord64

This commit is contained in:
iZePlayz 2025-12-11 17:50:31 +01:00
parent 4f9a51ff90
commit a07102ba11
13 changed files with 1063 additions and 10 deletions

View file

@ -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()

434
mods/bungeecord64/main.lua Normal file
View file

@ -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 <port>\#aaaaaa\ - Switch to server on port\n\#ffffff\/leave\#aaaaaa\ - Return to home server\n\#ffffff\/setfallback <port>\#aaaaaa\ - Set default home server\n\#ffffff\/setserverfallback <port>\#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 <port> : 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 <PORT>..." 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 <port>", "\\#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 <port>.", "\\#ff6666\\")
return true
end
performSwitch(target, "leave")
return true
end
local function cmdSetFallback(msg)
if msg == "" or msg == nil then
chatMsg("Usage: /setfallback <port>", "\\#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 <port>", "\\#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 <port> <name>", "\\#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 <port> 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 <port> - Switch to server", "\\#aaaaaa\\")
chatMsg("/leave - Return to home server", "\\#aaaaaa\\")
chatMsg("/setfallback <port> - Set client home", "\\#aaaaaa\\")
if server then
chatMsg("/setserverfallback <port> - Set server fallback", "\\#aaaaaa\\")
end
chatMsg("/addserver <port> <name> - 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 <port> - Switch to server", cmdSwitch)
hook_chat_command("leave", "[BC64] /leave - Return to home server", cmdLeave)
hook_chat_command("setfallback", "[BC64] /setfallback <port> - Set home server", cmdSetFallback)
hook_chat_command("setserverfallback", "[BC64] /setserverfallback <port> - Set server fallback (host only)", cmdSetServerFallback)
hook_chat_command("addserver", "[BC64] /addserver <port> <name> - Register server", cmdAddServer)
init()

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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();

View file

@ -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

View file

@ -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;
}
}

View file

@ -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]);

View file

@ -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

View file

@ -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 <stdio.h>
#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);
}

View file

@ -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;

View file

@ -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