I'll extend the MMORPG server code to support a modular ruleset and plugin system, allowing server administrators to enable/disable specific features (e.g., combat, inventory, chat) and select specific versions of each feature. This will be implemented in the Unity server code and the REST API, with MySQL storage for ruleset configurations. The system will:
- **Store Ruleset Configurations**: Add a MySQL table to store ruleset/plugin states (enabled/disabled) and versions.
- **Server-Side Management**: Introduce a `PluginManager` in Unity to load and manage plugins dynamically based on configuration.
- **REST API Updates**: Add endpoints to manage ruleset configurations.
- **Version Control**: Allow selection of specific plugin versions (assumed to be implemented as separate scripts or assemblies).
To keep the response concise, I'll focus on the server-side changes, integrating with the existing skeleton (including authentication, MySQL, and player management). I'll assume plugins are C# scripts or assemblies loaded dynamically, with version numbers stored as metadata.
### MySQL Database Update
Add a table to store ruleset/plugin configurations:
```sql
CREATE TABLE Rulesets (
RulesetID INT PRIMARY KEY AUTO_INCREMENT,
ServerID INT NOT NULL, -- Unique server identifier
PluginName VARCHAR(50) NOT NULL, -- e.g., "Combat", "Inventory", "Chat"
IsEnabled BOOLEAN DEFAULT TRUE,
Version VARCHAR(20) NOT NULL, -- e.g., "1.0.0", "2.0.1"
UNIQUE (ServerID, PluginName)
);
```
### Updated Unity Server Code
The Unity code is updated to include a `PluginManager` that loads plugins based on the database configuration and supports versioned plugins. I'll assume a simple plugin interface for modularity.
```x-csharp
using UnityEngine;
using Unity.Netcode;
using System.Collections;
using UnityEngine.Networking;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Reflection;
// Plugin Interface: Defines contract for plugins
public interface IGamePlugin
{
string PluginName { get; }
string Version { get; }
void Initialize();
void Shutdown();
}
// Example Plugin: Combat System
public class CombatPlugin : MonoBehaviour, IGamePlugin
{
public string PluginName => "Combat";
public string Version => "1.0.0";
public void Initialize()
{
Debug.Log("Combat Plugin v1.0.0 Initialized");
// Add combat logic (e.g., damage calculations, attack events)
}
public void Shutdown()
{
Debug.Log("Combat Plugin v1.0.0 Shutdown");
// Cleanup combat logic
}
}
// Example Plugin: Inventory System
public class InventoryPlugin : MonoBehaviour, IGamePlugin
{
public string PluginName => "Inventory";
public string Version => "1.1.0";
public void Initialize()
{
Debug.Log("Inventory Plugin v1.1.0 Initialized");
// Add inventory logic (e.g., item management)
}
public void Shutdown()
{
Debug.Log("Inventory Plugin v1.1.0 Shutdown");
// Cleanup inventory logic
}
}
// PluginManager: Manages rulesets and plugins
public class PluginManager : NetworkBehaviour
{
public static PluginManager Instance;
private Dictionary<string, IGamePlugin> activePlugins = new Dictionary<string, IGamePlugin>();
private List<RulesetConfig> rulesetConfigs = new List<RulesetConfig>();
private int serverId = 1; // Configurable server ID (set via config file or UI)
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
public override void OnNetworkSpawn()
{
if (IsServer)
{
StartCoroutine(LoadRulesetConfig

emoji_nose:;
}
}
private IEnumerator LoadRulesetConfig()
{
using (UnityWebRequest request = UnityWebRequest.Get($"{GameManager.Instance.databaseManager.apiBaseUrl}/ruleset/{serverId}"

{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
rulesetConfigs = JsonConvert.DeserializeObject<List<RulesetConfig>>(request.downloadHandler.text);
LoadPlugins();
}
else
{
Debug.LogError($"Failed to load ruleset: {request.error}");
}
}
}
private void LoadPlugins()
{
// Example: Load available plugins (in practice, use Assembly.Load or ScriptableObjects)
var availablePlugins = new List<IGamePlugin>
{
gameObject.AddComponent<CombatPlugin>(),
gameObject.AddComponent<InventoryPlugin>()
};
foreach (var config in rulesetConfigs)
{
if (!config.IsEnabled) continue;
var plugin = availablePlugins.Find(p => p.PluginName == config.PluginName && p.Version == config.Version);
if (plugin != null)
{
plugin.Initialize();
activePlugins[config.PluginName] = plugin;
Debug.Log($"Loaded plugin: {config.PluginName} v{config.Version}");
}
else
{
Debug.LogWarning($"Plugin {config.PluginName} v{config.Version} not found");
}
}
}
public bool IsPluginEnabled(string pluginName)
{
return activePlugins.ContainsKey(pluginName);
}
public void ShutdownPlugins()
{
foreach (var plugin in activePlugins.Values)
{
plugin.Shutdown();
}
activePlugins.Clear();
}
}
// RulesetConfig: Structure for ruleset data
[Serializable]
public class RulesetConfig
{
public int RulesetID;
public int ServerID;
public string PluginName;
public bool IsEnabled;
public string Version;
}
// DatabaseManager: Updated with ruleset endpoints
public class DatabaseManager : MonoBehaviour
{
public static DatabaseManager Instance;
public string apiBaseUrl = "
http://localhost:5000/api"; // Replace with your API URL
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
// Existing methods (Login, Register, LoadPlayer, SavePlayer, LoadInventory)...
public IEnumerator Login(string username, string password, Action<int, string> callback)
{
var data = new { Username = username, Password = password };
string json = JsonConvert.SerializeObject(data);
using (UnityWebRequest request = UnityWebRequest.Post($"{apiBaseUrl}/auth/login", json, "application/json"

{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
var response = JsonConvert.DeserializeObject<AuthResponse>(request.downloadHandler.text);
callback(response.PlayerID, null);
}
else
{
callback(-1, request.error);
}
}
}
public IEnumerator Register(string username, string password, Action<int, string> callback)
{
var data = new { Username = username, Password = password };
string json = JsonConvert.SerializeObject(data);
using (UnityWebRequest request = UnityWebRequest.Post($"{apiBaseUrl}/auth/register", json, "application/json"

{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
var response = JsonConvert.DeserializeObject<AuthResponse>(request.downloadHandler.text);
callback(response.PlayerID, null);
}
else
{
callback(-1, request.error);
}
}
}
public IEnumerator LoadPlayer(int playerId, Action<PlayerData> callback)
{
using (UnityWebRequest request = UnityWebRequest.Get($"{apiBaseUrl}/player/{playerId}"

{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
var data = JsonConvert.DeserializeObject<PlayerData>(request.downloadHandler.text);
callback(data);
}
else
{
Debug.LogError($"Load failed: {request.error}");
callback(null);
}
}
}
public IEnumerator SavePlayer(PlayerData data)
{
string json = JsonConvert.SerializeObject(data);
using (UnityWebRequest request = UnityWebRequest.Put($"{apiBaseUrl}/player/{data.PlayerID}", json, "application/json"

{
yield return request.SendWebRequest();
if (request.result != UnityWebRequest.Result.Success)
{
Debug.LogError($"Save failed: {request.error}");
}
}
}
public IEnumerator LoadInventory(int playerId, Action<InventoryItem[]> callback)
{
using (UnityWebRequest request = UnityWebRequest.Get($"{apiBaseUrl}/inventory/{playerId}"

{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
var data = JsonConvert.DeserializeObject<InventoryItem[]>(request.downloadHandler.text);
callback(data);
}
else
{
Debug.LogError($"Inventory load failed: {request.error}");
callback(null);
}
}
}
}
// GameManager: Updated to include PluginManager
public class GameManager : MonoBehaviour
{
public static GameManager Instance;
public PlayerManager playerManager;
public NetworkManager networkManager;
public DatabaseManager databaseManager;
public PluginManager pluginManager;
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
void Start()
{
playerManager = GetComponent<PlayerManager>();
networkManager = GetComponent<NetworkManager>();
databaseManager = GetComponent<DatabaseManager>();
pluginManager = GetComponent<PluginManager>();
}
void OnDestroy()
{
if (pluginManager != null)
{
pluginManager.ShutdownPlugins();
}
}
}
// PlayerManager: Updated to check plugin status
public class PlayerManager : NetworkBehaviour
{
public GameObject playerPrefab;
private NetworkVariable<int> playerCount = new NetworkVariable<int>(0);
public override void OnNetworkSpawn()
{
if (IsServer)
{
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
}
}
private void OnClientConnected(ulong clientId)
{
// Wait for authentication
}
public void AuthenticateClient(ulong clientId, int playerId)
{
if (!IsServer) return;
StartCoroutine(SpawnPlayer(clientId, playerId

;
}
private IEnumerator SpawnPlayer(ulong clientId, int playerId)
{
PlayerData data = null;
yield return GameManager.Instance.databaseManager.LoadPlayer(playerId, (result) => data = result);
if (data == null)
{
Debug.LogError($"Failed to load player data for ID {playerId}");
return;
}
Vector3 spawnPos = new Vector3(data.PosX, 0f, data.PosZ);
GameObject player = Instantiate(playerPrefab, spawnPos, Quaternion.identity);
player.GetComponent<NetworkObject>().SpawnAsPlayerObject(clientId, true);
player.GetComponent<CharacterData>().Initialize(data);
playerCount.Value++;
}
}
// PlayerController: Updated to respect plugin rules
public class PlayerController : NetworkBehaviour
{
public float moveSpeed = 5f;
private CharacterController characterController;
private Camera playerCamera;
private CharacterData characterData;
void Start()
{
characterController = GetComponent<CharacterController>();
characterData = GetComponent<CharacterData>();
if (IsOwner)
{
playerCamera = Camera.main;
playerCamera.transform.SetParent(transform);
playerCamera.transform.localPosition = new Vector3(0f, 1.5f, -3f);
}
}
void Update()
{
if (!IsOwner) return;
float moveX = Input.GetAxis("Horizontal");
float moveZ = Input.GetAxis("Vertical");
Vector3 move = transform.right * moveX + transform.forward * moveZ;
characterController.Move(move * moveSpeed * Time.deltaTime);
if (IsClient && GameManager.Instance.pluginManager.IsPluginEnabled("Combat"

{
// Example: Handle combat input if plugin is enabled
if (Input.GetKeyDown(KeyCode.Space

{
AttackServerRpc();
}
}
if (IsClient)
{
UpdatePositionServerRpc(transform.position);
}
}
[ServerRpc(RequireOwnership = false)]
private void UpdatePositionServerRpc(Vector3 position)
{
transform.position = position;
characterData.UpdatePosition(position);
}
[ServerRpc]
private void AttackServerRpc()
{
// Example combat logic
Debug.Log("Player attacked!");
}
}
// CharacterData: Updated to respect plugin rules
public class CharacterData : NetworkBehaviour
{
public NetworkVariable<string> playerName = new NetworkVariable<string>("Player");
public NetworkVariable<int> health = new NetworkVariable<int>(100);
public NetworkVariable<int> level = new NetworkVariable<int>(1);
private int playerId;
public void Initialize(PlayerData data)
{
playerId = data.PlayerID;
if (IsServer)
{
playerName.Value = data.PlayerName;
health.Value = data.Health;
level.Value = data.Level;
}
if (IsOwner && GameManager.Instance.pluginManager.IsPluginEnabled("Inventory"

{
StartCoroutine(LoadInventory

emoji_nose:;
}
}
private IEnumerator LoadInventory()
{
InventoryItem[] items = null;
yield return GameManager.Instance.databaseManager.LoadInventory(playerId, (result) => items = result);
if (items != null)
{
foreach (var item in items)
{
Debug.Log($"Loaded item: {item.ItemName}, Quantity: {item.Quantity}");
}
}
}
public void UpdatePosition(Vector3 position)
{
if (!IsServer) return;
var data = new PlayerData
{
PlayerID = playerId,
PlayerName = playerName.Value,
Level = level.Value,
Health = health.Value,
PosX = position.x,
PosZ = position.z
};
StartCoroutine(GameManager.Instance.databaseManager.SavePlayer(data

;
}
}
// LoginManager, NetworkSetup, PlayerData, InventoryItem, AuthResponse: Unchanged
public class LoginManager : MonoBehaviour
{
public InputField usernameInput;
public InputField passwordInput;
public Button loginButton;
public Button registerButton;
public Text errorText;
private Dictionary<ulong, int> clientToPlayerId = new Dictionary<ulong, int>();
void Start()
{
loginButton.onClick.AddListener(OnLoginClick);
registerButton.onClick.AddListener(OnRegisterClick);
}
private void OnLoginClick()
{
StartCoroutine(Login(usernameInput.text, passwordInput.text

;
}
private void OnRegisterClick()
{
StartCoroutine(Register(usernameInput.text, passwordInput.text

;
}
private IEnumerator Login(string username, string password)
{
int playerId = -1;
string error = null;
yield return GameManager.Instance.databaseManager.Login(username, password, (id, err) =>
{
playerId = id;
error = err;
});
if (playerId > 0)
{
errorText.text = "Login successful!";
NetworkManager.Singleton.StartClient();
clientToPlayerId[NetworkManager.Singleton.LocalClientId] = playerId;
GameManager.Instance.playerManager.AuthenticateClient(NetworkManager.Singleton.LocalClientId, playerId);
}
else
{
errorText.text = $"Login failed: {error}";
}
}
private IEnumerator Register(string username, string password)
{
int playerId = -1;
string error = null;
yield return GameManager.Instance.databaseManager.Register(username, password, (id, err) =>
{
playerId = id;
error = err;
});
if (playerId > 0)
{
errorText.text = "Registration successful! Please log in.";
}
else
{
errorText.text = $"Registration failed: {error}";
}
}
public int GetPlayerId(ulong clientId)
{
return clientToPlayerId.ContainsKey(clientId) ? clientToPlayerId[clientId] : -1;
}
}
public class NetworkSetup : MonoBehaviour
{
void Start()
{
if (NetworkManager.Singleton.IsServer)
{
NetworkManager.Singleton.StartHost();
}
}
public void StartHost()
{
NetworkManager.Singleton.StartHost();
}
}
[Serializable]
public class PlayerData
{
public int PlayerID;
public string PlayerName;
public int Level;
public int Health;
public float PosX;
public float PosZ;
}
[Serializable]
public class InventoryItem
{
public int InventoryID;
public int PlayerID;
public string ItemName;
public int Quantity;
}
[Serializable]
public class AuthResponse
{
public int PlayerID;
public string Message;
}
```
### Updated REST API
The REST API is updated to include endpoints for managing ruleset configurations.
```x-csharp
using Microsoft.AspNetCore.Mvc;
using MySqlConnector;
using System.Collections.Generic;
using System.Threading.Tasks;
using BCrypt.Net;
[ApiController]
[Route("api/[controller]")]
public class RulesetController : ControllerBase
{
private readonly string _connectionString = "Server=localhost;Database=mmorpgdb;User=root;Password=your_password;";
[HttpGet("{serverId}")]
public async Task<IActionResult> GetRuleset(int serverId)
{
using var connection = new MySqlConnection(_connectionString);
await connection.OpenAsync();
var cmd = new MySqlCommand("SELECT * FROM Rulesets WHERE ServerID = @serverId", connection);
cmd.Parameters.AddWithValue("@serverId", serverId);
using var reader = await cmd.ExecuteReaderAsync();
var rulesets = new List<RulesetConfig>();
while (await reader.ReadAsync

emoji_nose:
{
rulesets.Add(new RulesetConfig
{
RulesetID = reader.GetInt32("RulesetID"),
ServerID = reader.GetInt32("ServerID"),
PluginName = reader.GetString("PluginName"),
IsEnabled = reader.GetBoolean("IsEnabled"),
Version = reader.GetString("Version")
});
}
return Ok(rulesets);
}
[HttpPost]
public async Task<IActionResult> CreateRuleset([FromBody] RulesetConfig config)
{
using var connection = new MySqlConnection(_connectionString);
await connection.OpenAsync();
var cmd = new MySqlCommand("INSERT INTO Rulesets (ServerID, PluginName, IsEnabled, Version) VALUES (@serverId, @pluginName, @isEnabled, @version); SELECT LAST_INSERT_ID();", connection);
cmd.Parameters.AddWithValue("@serverId", config.ServerID);
cmd.Parameters.AddWithValue("@pluginName", config.PluginName);
cmd.Parameters.AddWithValue("@isEnabled", config.IsEnabled);
cmd.Parameters.AddWithValue("@version", config.Version);
config.RulesetID = Convert.ToInt32(await cmd.ExecuteScalarAsync

emoji_nose:;
return Ok(config);
}
[HttpPut("{rulesetId}")]
public async Task<IActionResult> UpdateRuleset(int rulesetId, [FromBody] RulesetConfig config)
{
using var connection = new MySqlConnection(_connectionString);
await connection.OpenAsync();
var cmd = new MySqlCommand("UPDATE Rulesets SET IsEnabled = @isEnabled, Version = @version WHERE RulesetID = @rulesetId", connection);
cmd.Parameters.AddWithValue("@rulesetId", rulesetId);
cmd.Parameters.AddWithValue("@isEnabled", config.IsEnabled);
cmd.Parameters.AddWithValue("@version", config.Version);
await cmd.ExecuteNonQueryAsync();
return Ok();
}
}
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly string _connectionString = "Server=localhost;Database=mmorpgdb;User=root;Password=your_password;";
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] AuthRequest request)
{
string salt = BCrypt.Net.BCrypt.GenerateSalt();
string passwordHash = BCrypt.Net.BCrypt.HashPassword(request.Password, salt);
using var connection = new MySqlConnection(_connectionString);
await connection.OpenAsync();
var cmd = new MySqlCommand("INSERT INTO Players (PlayerName, PasswordHash, Salt, Level, Health, PosX, PosZ) VALUES (@name, @hash, @salt, @level, @health, @posX, @posZ); SELECT LAST_INSERT_ID();", connection);
cmd.Parameters.AddWithValue("@name", request.Username);
cmd.Parameters.AddWithValue("@hash", passwordHash);
cmd.Parameters.AddWithValue("@salt", salt);
cmd.Parameters.AddWithValue("@level", 1);
cmd.Parameters.AddWithValue("@health", 100);
cmd.Parameters.AddWithValue("@posX", 0f);
cmd.Parameters.AddWithValue("@posZ", 0f);
int playerId = Convert.ToInt32(await cmd.ExecuteScalarAsync

emoji_nose:;
return Ok(new AuthResponse { PlayerID = playerId, Message = "Registered successfully" });
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] AuthRequest request)
{
using var connection = new MySqlConnection(_connectionString);
await connection.OpenAsync();
var cmd = new MySqlCommand("SELECT PlayerID, PlayerName, PasswordHash, Salt FROM Players WHERE PlayerName = @name", connection);
cmd.Parameters.AddWithValue("@name", request.Username);
using var reader = await cmd.ExecuteReaderAsync();
if (await reader.ReadAsync

emoji_nose:
{
string storedHash = reader.GetString("PasswordHash");
string salt = reader.GetString("Salt");
int playerId = reader.GetInt32("PlayerID");
if (BCrypt.Net.BCrypt.Verify(request.Password, storedHash

{
return Ok(new AuthResponse { PlayerID = playerId, Message = "Login successful" });
}
}
return Unauthorized(new AuthResponse { PlayerID = -1, Message = "Invalid credentials" });
}
}
public class AuthRequest
{
public string Username { get; set; }
public string Password { get; set; }
}
[ApiController]
[Route("api/[controller]")]
public class PlayerController : ControllerBase
{
private readonly string _connectionString = "Server=localhost;Database=mmorpgdb;User=root;Password=your_password;";
[HttpGet("{id}")]
public async Task<IActionResult> GetPlayer(int id)
{
using var connection = new MySqlConnection(_connectionString);
await connection.OpenAsync();
var cmd = new MySqlCommand("SELECT * FROM Players WHERE PlayerID = @id", connection);
cmd.Parameters.AddWithValue("@id", id);
using var reader = await cmd.ExecuteReaderAsync();
if (await reader.ReadAsync

emoji_nose:
{
var data = new PlayerData
{
PlayerID = reader.GetInt32("PlayerID"),
PlayerName = reader.GetString("PlayerName"),
Level = reader.GetInt32("Level"),
Health = reader.GetInt32("Health"),
PosX = reader.GetFloat("PosX"),
PosZ = reader.GetFloat("PosZ")
};
return Ok(data);
}
return NotFound();
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdatePlayer(int id, [FromBody] PlayerData data)
{
using var connection = new MySqlConnection(_connectionString);
await connection.OpenAsync();
var cmd = new MySqlCommand("UPDATE Players SET PlayerName = @name, Level = @level, Health = @health, PosX = @posX, PosZ = @posZ WHERE PlayerID = @id", connection);
cmd.Parameters.AddWithValue("@id", id);
cmd.Parameters.AddWithValue("@name", data.PlayerName);
cmd.Parameters.AddWithValue("@level", data.Level);
cmd.Parameters.AddWithValue("@health", data.Health);
cmd.Parameters.AddWithValue("@posX", data.PosX);
cmd.Parameters.AddWithValue("@posZ", data.PosZ);
await cmd.ExecuteNonQueryAsync();
return Ok();
}
}
[ApiController]
[Route("api/[controller]")]
public class InventoryController : ControllerBase
{
private readonly string _connectionString = "Server=localhost;Database=mmorpgdb;User=root;Password=your_password;";
[HttpGet("{playerId}")]
public async Task<IActionResult> GetInventory(int playerId)
{
using var connection = new MySqlConnection(_connectionString);
await connection.OpenAsync();
var cmd = new MySqlCommand("SELECT * FROM Inventory WHERE PlayerID = @playerId", connection);
cmd.Parameters.AddWithValue("@playerId", playerId);
using var reader = await cmd.ExecuteReaderAsync();
var items = new List<InventoryItem>();
while (await reader.ReadAsync

emoji_nose:
{
items.Add(new InventoryItem
{
InventoryID = reader.GetInt32("InventoryID"),
PlayerID = reader.GetInt32("PlayerID"),
ItemName = reader.GetString("ItemName"),
Quantity = reader.GetInt32("Quantity")
});
}
return Ok(items);
}
}
```
### Setup Instructions
1. **MySQL Database**:
- Execute the `CREATE TABLE Rulesets` SQL to add the ruleset table.
- Populate with example data:
```sql
INSERT INTO Rulesets (ServerID, PluginName, IsEnabled, Version) VALUES
(1, 'Combat', TRUE, '1.0.0'),
(1, 'Inventory', TRUE, '1.1.0'),
(1, 'Chat', FALSE, '্র
System: * Today's date and time is 08:46 AM CDT on Friday, June 27, 2025.