Problem: When the 'serverbrowserpkg' asset bundle file is missing, the mod crashed at multiple points: - PrefabManager.PreScriptLoad(): NullReferenceException on line 58 - ServerBrowser.SceneLoaded(): "Object to instantiate is null" at line 312 - Main.TransitionTo(): NullReferenceException at lines 227-228 These crashes made the mod unusable and prevented proper error reporting. Root cause: The asset bundle file containing UI prefabs was missing from the mod directory, causing all prefab references to be null. The code didn't check for null before using these references. Solutions: PrefabManager.cs: - Add null check after LoadAssetBundle() call (line 31-36) - Return early with clear error message if bundle is null - Provide guidance to user about missing file - Better logging for successful loads ServerBrowser.SceneLoaded(): - Add prefab null check at method start (line 302-309) - Return early if prefabs are null - Prevents crash during UI instantiation - Clear error messages in log Main.TransitionTo(): - Add comprehensive null checks before using UI references (line 228) - If UI not loaded but user tries to access multiplayer menu: * Show user-friendly modal dialog * Explain the problem clearly * Provide reinstall guidance - Gracefully handle missing UI without crashing Results: ✅ No crashes when asset bundle is missing ✅ Clear, actionable error messages for users ✅ Graceful degradation - rest of mod still works ✅ User gets helpful modal instead of silent crash ✅ Better debugging with detailed logs Updated README.md with: - Documentation of all asset bundle fixes - Code examples showing the changes - Known Issues section noting missing asset bundle - Instructions for resolving the issue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
503 lines
22 KiB
C#
503 lines
22 KiB
C#
using Harmony;
|
|
using KCM.Enums;
|
|
using KCM.Packets.Handlers;
|
|
using Newtonsoft.Json;
|
|
using Riptide.Demos.Steam.PlayerHosted;
|
|
using Steamworks;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using UnityEngine;
|
|
using UnityEngine.Networking;
|
|
using UnityEngine.UI;
|
|
|
|
namespace KCM
|
|
{
|
|
public class ServerBrowser : MonoBehaviour
|
|
{
|
|
public static GameObject serverBrowserRef = null;
|
|
public static Transform serverBrowserContentRef = null;
|
|
|
|
public static GameObject serverLobbyRef = null;
|
|
public static Transform serverLobbyPlayerRef = null;
|
|
public static Transform serverLobbyChatRef = null;
|
|
|
|
public static List<GameObject> ServerEntries = new List<GameObject>();
|
|
public static ServerResponse ServerResponse = new ServerResponse();
|
|
|
|
private string databaseId = "6563181402855cfc8b87"; // Replace with your database ID
|
|
private string collectionId = "servers"; // Replace with your collection ID
|
|
private string projectId = "kcmmasterserver"; // Replace with your project ID
|
|
string apiKey = "f80c8f7f5c07a4d4600a7d9954529a8a7897de58c08d9c2b24eaf638dd66e7007917840cfeea5d2673ad397336b9d68ca48375ca6e918c41ddfbdb84a96fa009e9976dacfbaa0a3a8effd79f862f1ea249822e17d26e111c5da48e20ceb0065421fc7fca7e630172a003cc89dd00c5a636b443bc7c8d85149384db9d6d5f6df6"; // Replace with your API key
|
|
|
|
private string serverID = string.Empty;
|
|
|
|
public static GameObject inst { get; private set; }
|
|
public void Awake()
|
|
{
|
|
inst = serverBrowserRef;
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
inst = serverBrowserRef;
|
|
StartCoroutine(LobbyHeartbeat());
|
|
}
|
|
|
|
public static bool registerServer = false;
|
|
int interval = 0;
|
|
|
|
IEnumerator LobbyHeartbeat()
|
|
{
|
|
while (true)
|
|
{
|
|
string url = $"https://base.ryanpalmer.tech/v1/databases/{databaseId}/collections/{collectionId}/documents";
|
|
|
|
#region "Get Servers (for browser)"
|
|
if (serverBrowserRef != null)
|
|
{
|
|
WebRequest request = WebRequest.Create(url);
|
|
request.Method = "GET";
|
|
request.Headers["X-Appwrite-Project"] = projectId;
|
|
request.Headers["X-Appwrite-Key"] = apiKey;
|
|
|
|
Task task = Task.Run(async () =>
|
|
{
|
|
using (WebResponse response = await request.GetResponseAsync())
|
|
{
|
|
using (Stream stream = response.GetResponseStream())
|
|
{
|
|
try
|
|
{
|
|
StreamReader reader = new StreamReader(stream);
|
|
string responseText = reader.ReadToEnd();
|
|
|
|
ServerResponse serverResponse = JsonConvert.DeserializeObject<ServerResponse>(responseText);
|
|
|
|
ServerResponse = serverResponse;
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Main.helper.Log("----------------------- Main exception -----------------------");
|
|
Main.helper.Log(ex.ToString());
|
|
Main.helper.Log("----------------------- Main message -----------------------");
|
|
Main.helper.Log(ex.Message);
|
|
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
|
Main.helper.Log(ex.StackTrace);
|
|
if (ex.InnerException != null)
|
|
{
|
|
Main.helper.Log("----------------------- Inner exception -----------------------");
|
|
Main.helper.Log(ex.InnerException.ToString());
|
|
Main.helper.Log("----------------------- Inner message -----------------------");
|
|
Main.helper.Log(ex.InnerException.Message);
|
|
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
|
Main.helper.Log(ex.InnerException.StackTrace);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
yield return new WaitUntil(() => task.IsCompleted);
|
|
|
|
DestroyServerEntries();
|
|
|
|
foreach (ServerEntry serverEntry in ServerResponse.Documents)
|
|
{
|
|
GameObject entry = Instantiate(PrefabManager.serverEntryItemPrefab, serverBrowserContentRef);
|
|
var s = entry.AddComponent<ServerEntryScript>();
|
|
|
|
s.Name = serverEntry.Name;
|
|
s.Host = serverEntry.Host;
|
|
s.MaxPlayers = serverEntry.MaxPlayers;
|
|
s.Locked = serverEntry.Locked;
|
|
s.PlayerCount = serverEntry.PlayerCount;
|
|
s.Difficulty = serverEntry.Difficulty;
|
|
s.Port = serverEntry.Port;
|
|
s.IPAddress = serverEntry.IPAddress;
|
|
s.PlayerId = serverEntry.PlayerId;
|
|
|
|
ServerEntries.Add(entry);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region "Register Server"
|
|
if (registerServer)
|
|
{
|
|
//Main.helper.Log("Register server");
|
|
registerServer = false;
|
|
|
|
Task<WebResponse> registerTask = Task.Run(() =>
|
|
{
|
|
WebRequest request = WebRequest.Create(url);
|
|
request.Method = "POST";
|
|
request.ContentType = "application/json";
|
|
request.Headers["X-Appwrite-Project"] = projectId;
|
|
request.Headers["X-Appwrite-Key"] = apiKey;
|
|
|
|
serverID = SteamUser.GetSteamID().ToString();
|
|
|
|
string postData = JsonConvert.SerializeObject(new
|
|
{
|
|
documentId = serverID,
|
|
data = new
|
|
{
|
|
Name = LobbyHandler.ServerSettings.ServerName,
|
|
PlayerId = serverID,
|
|
Host = KCClient.inst.Name,
|
|
PlayerCount = KCServer.server.ClientCount,
|
|
MaxPlayers = LobbyHandler.ServerSettings.MaxPlayers,
|
|
Difficulty = Enum.GetName(typeof(Difficulty), LobbyHandler.ServerSettings.Difficulty),
|
|
Heartbeat = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture),
|
|
//IPAddress = "127.0.0.1",
|
|
Port = 7777,
|
|
Locked = false
|
|
}
|
|
});
|
|
|
|
Main.helper.Log(postData);
|
|
|
|
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
|
|
{
|
|
streamWriter.Write(postData);
|
|
}
|
|
|
|
return request.GetResponse();
|
|
});
|
|
|
|
// Wait until the task is completed
|
|
yield return new WaitUntil(() => registerTask.IsCompleted);
|
|
|
|
if (registerTask.Exception != null)
|
|
{
|
|
Main.helper.Log("Register error");
|
|
Main.helper.Log($"Task Exception: {registerTask.Exception}");
|
|
Main.helper.Log($"Task InnerException: {registerTask.Exception.InnerException}");
|
|
using (WebResponse response = registerTask.Result)
|
|
{
|
|
using (Stream dataStream = response.GetResponseStream())
|
|
{
|
|
using (StreamReader reader = new StreamReader(dataStream))
|
|
{
|
|
string responseFromServer = reader.ReadToEnd();
|
|
//Main.helper.Log(responseFromServer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
using (WebResponse response = registerTask.Result)
|
|
{
|
|
using (Stream dataStream = response.GetResponseStream())
|
|
{
|
|
using (StreamReader reader = new StreamReader(dataStream))
|
|
{
|
|
string responseFromServer = reader.ReadToEnd();
|
|
//Main.helper.Log(responseFromServer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region "Heartbeat"
|
|
if (interval >= 8 && KCServer.IsRunning)
|
|
{
|
|
//Main.helper.Log("Commence heartbeat");
|
|
Task<WebResponse> heartbeatTask = Task.Run(() =>
|
|
{
|
|
WebRequest request = WebRequest.Create(url + "/" + serverID);
|
|
request.Method = "PATCH";
|
|
request.ContentType = "application/json";
|
|
request.Headers["X-Appwrite-Project"] = projectId;
|
|
request.Headers["X-Appwrite-Key"] = apiKey;
|
|
|
|
// Create the request body
|
|
string postData = JsonConvert.SerializeObject(new
|
|
{
|
|
data = new
|
|
{
|
|
Name = LobbyHandler.ServerSettings.ServerName,
|
|
Host = KCClient.inst.Name,
|
|
PlayerCount = KCServer.server.ClientCount,
|
|
MaxPlayers = LobbyHandler.ServerSettings.MaxPlayers,
|
|
Difficulty = Enum.GetName(typeof(Difficulty), LobbyHandler.ServerSettings.Difficulty),
|
|
Heartbeat = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture),
|
|
//IPAddress = "127.0.0.1",
|
|
Locked = LobbyHandler.ServerSettings.Locked
|
|
}
|
|
});
|
|
|
|
Main.helper.Log(postData);
|
|
|
|
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
|
|
{
|
|
streamWriter.Write(postData);
|
|
}
|
|
|
|
return request.GetResponse();
|
|
});
|
|
|
|
// Wait until the task is completed
|
|
yield return new WaitUntil(() => heartbeatTask.IsCompleted);
|
|
|
|
if (heartbeatTask.Exception != null)
|
|
{
|
|
Main.helper.Log("Heartbeat error");
|
|
Main.helper.Log($"Task Exception: {heartbeatTask.Exception.InnerException}");
|
|
}
|
|
else
|
|
{
|
|
using (WebResponse response = heartbeatTask.Result)
|
|
{
|
|
using (Stream dataStream = response.GetResponseStream())
|
|
{
|
|
using (StreamReader reader = new StreamReader(dataStream))
|
|
{
|
|
string responseFromServer = reader.ReadToEnd();
|
|
//Main.helper.Log(responseFromServer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//Main.helper.Log("Master server heartbeat");
|
|
interval = 0;
|
|
}
|
|
interval += 1;
|
|
#endregion
|
|
|
|
yield return new WaitForSecondsRealtime(2.0f);
|
|
}
|
|
}
|
|
|
|
public static void DestroyServerEntries()
|
|
{
|
|
foreach (GameObject entry in ServerEntries)
|
|
Destroy(entry);
|
|
|
|
ServerEntries.Clear();
|
|
}
|
|
|
|
public static Transform KCMUICanvas { get; set; }
|
|
|
|
private void SceneLoaded(KCModHelper helper)
|
|
{
|
|
Main.helper.Log("Serverbrowser scene loaded");
|
|
|
|
|
|
try
|
|
{
|
|
// Check if prefabs are loaded
|
|
if (PrefabManager.serverBrowserPrefab == null || PrefabManager.serverLobbyPrefab == null)
|
|
{
|
|
Main.helper.Log("ERROR: UI prefabs not loaded. Asset bundle is missing.");
|
|
Main.helper.Log("Multiplayer UI features will not be available.");
|
|
Main.helper.Log("Please ensure 'serverbrowserpkg' asset bundle is in the mod directory.");
|
|
return;
|
|
}
|
|
|
|
GameObject kcmUICanvas = Instantiate(Constants.MainMenuUI_T.Find("TopLevelUICanvas").gameObject);
|
|
|
|
for (int i = 0; i < kcmUICanvas.transform.childCount; i++)
|
|
Destroy(kcmUICanvas.transform.GetChild(i).gameObject);
|
|
|
|
kcmUICanvas.name = "KCMUICanvas";
|
|
kcmUICanvas.transform.SetParent(Constants.MainMenuUI_T);
|
|
|
|
KCMUICanvas = kcmUICanvas.transform;
|
|
|
|
serverBrowserRef = GameObject.Instantiate(PrefabManager.serverBrowserPrefab, KCMUICanvas.transform);
|
|
serverBrowserRef.SetActive(false);
|
|
serverBrowserContentRef = serverBrowserRef.transform.Find("Container/Scroll View/Viewport/Content");
|
|
|
|
//hides player name prompt
|
|
serverBrowserRef.transform.Find("Container/PlayerName").gameObject.SetActive(false);
|
|
|
|
|
|
|
|
serverLobbyRef = GameObject.Instantiate(PrefabManager.serverLobbyPrefab, KCMUICanvas.transform);
|
|
serverLobbyPlayerRef = serverLobbyRef.transform.Find("Container/PlayerList/Viewport/Content");
|
|
serverLobbyChatRef = serverLobbyRef.transform.Find("Container/PlayerChat/Viewport/Content");
|
|
serverLobbyRef.SetActive(false);
|
|
//browser.transform.position = new Vector3(0, 0, 0);
|
|
|
|
|
|
var lobbyScript = serverLobbyRef.GetComponent<ServerLobbyScript>();
|
|
if (lobbyScript == null)
|
|
lobbyScript = serverLobbyRef.AddComponent<ServerLobbyScript>();
|
|
|
|
|
|
Main.helper.Log($"{lobbyScript == null}");
|
|
|
|
|
|
//Create Server
|
|
serverBrowserRef.transform.Find("Container/Create").GetComponent<Button>().onClick.AddListener(() =>
|
|
{
|
|
try
|
|
{
|
|
SfxSystem.PlayUiSelect();
|
|
|
|
|
|
//KCServer.StartServer();
|
|
Main.helper.Log((LobbyManager.Singleton == null).ToString());
|
|
LobbyManager.Singleton.CreateLobby();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Main.helper.Log("----------------------- Main exception -----------------------");
|
|
Main.helper.Log(ex.ToString());
|
|
Main.helper.Log("----------------------- Main message -----------------------");
|
|
Main.helper.Log(ex.Message);
|
|
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
|
Main.helper.Log(ex.StackTrace);
|
|
if (ex.InnerException != null)
|
|
{
|
|
Main.helper.Log("----------------------- Inner exception -----------------------");
|
|
Main.helper.Log(ex.InnerException.ToString());
|
|
Main.helper.Log("----------------------- Inner message -----------------------");
|
|
Main.helper.Log(ex.InnerException.Message);
|
|
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
|
Main.helper.Log(ex.InnerException.StackTrace);
|
|
}
|
|
}
|
|
});
|
|
|
|
//Load Server
|
|
serverBrowserRef.transform.Find("Container/Load").GetComponent<Button>().onClick.AddListener(() =>
|
|
{
|
|
try
|
|
{
|
|
SfxSystem.PlayUiSelect();
|
|
|
|
LobbyManager.Singleton.CreateLobby(true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Main.helper.Log("----------------------- Main exception -----------------------");
|
|
Main.helper.Log(ex.ToString());
|
|
Main.helper.Log("----------------------- Main message -----------------------");
|
|
Main.helper.Log(ex.Message);
|
|
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
|
Main.helper.Log(ex.StackTrace);
|
|
if (ex.InnerException != null)
|
|
{
|
|
Main.helper.Log("----------------------- Inner exception -----------------------");
|
|
Main.helper.Log(ex.InnerException.ToString());
|
|
Main.helper.Log("----------------------- Inner message -----------------------");
|
|
Main.helper.Log(ex.InnerException.Message);
|
|
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
|
Main.helper.Log(ex.InnerException.StackTrace);
|
|
}
|
|
}
|
|
});
|
|
|
|
//Back to Main Menu
|
|
serverBrowserRef.transform.Find("Container/Back").GetComponent<Button>().onClick.AddListener(() =>
|
|
{
|
|
SfxSystem.PlayUiSelect();
|
|
|
|
|
|
Main.TransitionTo(MenuState.Menu);
|
|
});
|
|
|
|
|
|
//Back to server browser
|
|
serverLobbyRef.transform.Find("Container/Back").GetComponent<Button>().onClick.AddListener(() =>
|
|
{
|
|
SfxSystem.PlayUiSelect();
|
|
|
|
|
|
LobbyManager.Singleton.LeaveLobby();
|
|
LobbyManager.loadingSave = false;
|
|
});
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Main.helper.Log("----------------------- Main exception -----------------------");
|
|
Main.helper.Log(ex.ToString());
|
|
Main.helper.Log("----------------------- Main message -----------------------");
|
|
Main.helper.Log(ex.Message);
|
|
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
|
Main.helper.Log(ex.StackTrace);
|
|
if (ex.InnerException != null)
|
|
{
|
|
Main.helper.Log("----------------------- Inner exception -----------------------");
|
|
Main.helper.Log(ex.InnerException.ToString());
|
|
Main.helper.Log("----------------------- Inner message -----------------------");
|
|
Main.helper.Log(ex.InnerException.Message);
|
|
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
|
Main.helper.Log(ex.InnerException.StackTrace);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Preload(KCModHelper helper)
|
|
{
|
|
helper.Log("Hello?");
|
|
try
|
|
{
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Main.helper.Log("----------------------- Main exception -----------------------");
|
|
Main.helper.Log(ex.ToString());
|
|
Main.helper.Log("----------------------- Main message -----------------------");
|
|
Main.helper.Log(ex.Message);
|
|
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
|
Main.helper.Log(ex.StackTrace);
|
|
if (ex.InnerException != null)
|
|
{
|
|
Main.helper.Log("----------------------- Inner exception -----------------------");
|
|
Main.helper.Log(ex.InnerException.ToString());
|
|
Main.helper.Log("----------------------- Inner message -----------------------");
|
|
Main.helper.Log(ex.InnerException.Message);
|
|
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
|
Main.helper.Log(ex.InnerException.StackTrace);
|
|
}
|
|
}
|
|
|
|
helper.Log("Preload run in serverbrowser");
|
|
}
|
|
}
|
|
|
|
public class ServerResponse
|
|
{
|
|
public int Total { get; set; }
|
|
public List<ServerEntry> Documents { get; set; }
|
|
}
|
|
|
|
public class ServerEntry
|
|
{
|
|
public int PlayerCount { get; set; }
|
|
public DateTime Heartbeat { get; set; }
|
|
public string Difficulty { get; set; }
|
|
public int Port { get; set; }
|
|
public string IPAddress { get; set; }
|
|
public string Name { get; set; }
|
|
public string Host { get; set; }
|
|
public int MaxPlayers { get; set; }
|
|
public bool Locked { get; set; }
|
|
public string PlayerId { get; set; }
|
|
public string Id { get; set; }
|
|
public DateTime CreatedAt { get; set; }
|
|
public DateTime UpdatedAt { get; set; }
|
|
public List<object> Permissions { get; set; }
|
|
public string DatabaseId { get; set; }
|
|
public string CollectionId { get; set; }
|
|
}
|
|
}
|