Files
K-C-Multiplayer/ServerBrowser/ServerBrowser.cs
devbeni 5e014a74da Fix NullReferenceException crashes when asset bundle is missing
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>
2025-12-14 21:24:55 +01:00

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