2026-09-15 18:13:46 +07:00
|
|
|
using Ananta.SDK.Logging;
|
|
|
|
|
using Ananta.SDK.Network;
|
|
|
|
|
using Ananta.SDK.Rpc;
|
2026-10-29 02:44:24 +07:00
|
|
|
using Ananta.Server.App.Admin;
|
2026-09-15 18:13:46 +07:00
|
|
|
using Ananta.Server.Configuration;
|
|
|
|
|
using Ananta.Server.Handlers.Game;
|
|
|
|
|
using Ananta.Server.Handlers.LoginGate;
|
|
|
|
|
using Ananta.Server.Protocol.Client4229938;
|
2026-09-18 14:19:52 +07:00
|
|
|
using Ananta.Server.Gameplay.Traffic;
|
|
|
|
|
using Ananta.Server.Gameplay.Crowd;
|
2026-10-29 01:55:01 +07:00
|
|
|
using Ananta.Server.Gameplay.World;
|
2026-09-15 18:13:46 +07:00
|
|
|
using Ananta.Server.Network;
|
|
|
|
|
|
|
|
|
|
namespace Ananta.Server.App;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Wires configuration, RPC routers and TCP listeners together.
|
|
|
|
|
/// Game behavior lives in Ananta.Handlers; protocol encoding lives in Ananta.Core/Protocol/Client4229938.
|
|
|
|
|
/// </summary>
|
|
|
|
|
internal sealed class PrivateServerApplication(PrivateServerConfig config)
|
|
|
|
|
{
|
|
|
|
|
internal async Task RunAsync(CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
var gateSessions = new GateSessionHub();
|
|
|
|
|
var gameSessions = new GameSessionHub();
|
|
|
|
|
var loginRouter = new LoginGateRouter(gateSessions).Build();
|
|
|
|
|
var gameRouter = new GameRouter(gateSessions).Build();
|
|
|
|
|
var loginPortA = config.Network.LoginPorts[0];
|
|
|
|
|
var loginPortB = config.Network.LoginPorts[1];
|
|
|
|
|
var bindHost = config.Network.BindHost;
|
|
|
|
|
|
|
|
|
|
var servers = new[]
|
|
|
|
|
{
|
|
|
|
|
CreateServer("login-0", bindHost, loginPortA, loginRouter),
|
|
|
|
|
CreateServer("login-1", bindHost, loginPortB, loginRouter),
|
|
|
|
|
CreateServer("game", bindHost, config.Network.GamePort, gameRouter, gameSessions),
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-18 14:19:52 +07:00
|
|
|
// Initialize Autonomous City Traffic Engine and Living Urban Crowd Engine
|
|
|
|
|
var clientDataFolder = Path.GetDirectoryName(PrivateServerConfigStore.ResolveProjectPath(config.Paths.ClientConfigs))
|
|
|
|
|
?? PrivateServerConfigStore.ResolveProjectPath(config.Paths.ClientConfigs);
|
|
|
|
|
|
|
|
|
|
CityTrafficEngine.Instance.Initialize(
|
|
|
|
|
clientDataFolder,
|
|
|
|
|
TrafficAndCrowdDriver.Instance,
|
|
|
|
|
() => gameSessions.Current is not null ? [gameSessions.Current] : Array.Empty<TcpSession>());
|
|
|
|
|
|
|
|
|
|
UrbanCrowdEngine.Instance.Initialize(
|
|
|
|
|
clientDataFolder,
|
|
|
|
|
TrafficAndCrowdDriver.Instance,
|
2026-10-29 01:55:01 +07:00
|
|
|
() => gameSessions.Current is not null ? [gameSessions.Current] : Array.Empty<TcpSession>());
|
|
|
|
|
|
2026-10-29 02:44:24 +07:00
|
|
|
// Interactive gadget + scene-item grid streaming
|
2026-10-29 01:55:01 +07:00
|
|
|
GadgetStreamingEngine.Instance.Initialize(
|
|
|
|
|
clientDataFolder,
|
|
|
|
|
TrafficAndCrowdDriver.Instance,
|
2026-09-18 14:19:52 +07:00
|
|
|
() => gameSessions.Current is not null ? [gameSessions.Current] : Array.Empty<TcpSession>());
|
|
|
|
|
|
2026-10-29 02:44:24 +07:00
|
|
|
// Localhost debug API for debug-panel / WebUI (port 5809)
|
2026-09-15 18:13:46 +07:00
|
|
|
using var debugApi = config.Debug.Enabled ? new DebugApiServer(config, gameSessions) : null;
|
|
|
|
|
var webInfo = "web=off";
|
|
|
|
|
if (debugApi is not null)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
debugApi.Start();
|
2026-10-29 02:44:24 +07:00
|
|
|
webInfo = $"http://{config.Debug.Host}:{config.Debug.Port}/";
|
2026-09-15 18:13:46 +07:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2026-10-29 02:44:24 +07:00
|
|
|
Console.WriteLine($"[DEBUG-API] disabled: {ex.Message}");
|
2026-09-15 18:13:46 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bootstrap diagnostics stay in console-latest/archive; runtime output becomes visible here.
|
|
|
|
|
RuntimeLogs.EndBootstrapQuietMode();
|
2026-10-29 02:44:24 +07:00
|
|
|
Console.WriteLine($"[READY] Ananta Private Server (HyNB v2.4 Modular) | client={config.Client.Version} | pid={Profile.PlayerPid} | login={config.Network.AdvertisedHost}:{loginPortA},{loginPortB} | game=:{config.Network.GamePort}");
|
|
|
|
|
if (config.Admin.Enabled)
|
|
|
|
|
Console.WriteLine($"[READY] Admin GM panel at http://{config.Admin.Host}:{config.Admin.Port}/");
|
|
|
|
|
if (webInfo != "web=off")
|
|
|
|
|
Console.WriteLine($"[READY] Debug WebUI panel at {webInfo}");
|
|
|
|
|
|
|
|
|
|
var adminTask = config.Admin.Enabled
|
|
|
|
|
? new AdminWebServer(config).RunAsync(cancellationToken)
|
|
|
|
|
: Task.CompletedTask;
|
2026-09-15 18:13:46 +07:00
|
|
|
|
|
|
|
|
var serverTasks = servers.Select(server => server.RunAsync(cancellationToken));
|
2026-10-29 02:44:24 +07:00
|
|
|
await Task.WhenAll(serverTasks.Prepend(adminTask));
|
2026-09-15 18:13:46 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static TcpServer CreateServer(string name, string host, int port, RpcRouter router, GameSessionHub? gameSessions = null)
|
2026-10-29 02:44:24 +07:00
|
|
|
{
|
|
|
|
|
var net = PrivateServerConfigStore.Current.Network;
|
|
|
|
|
return new(name, host, port, router,
|
2026-09-15 18:13:46 +07:00
|
|
|
async (session, frame, token) =>
|
|
|
|
|
{
|
|
|
|
|
gameSessions?.Touch(session);
|
2026-10-29 02:44:24 +07:00
|
|
|
GameAdminBridge.Touch(session);
|
2026-09-15 18:13:46 +07:00
|
|
|
await RpcFrameDispatcher.DispatchAsync(router, session, frame, token);
|
2026-10-29 02:44:24 +07:00
|
|
|
},
|
|
|
|
|
onClosed: null,
|
|
|
|
|
heartbeatMs: net.SocketHeartbeatMs,
|
|
|
|
|
writeTimeoutMs: net.WriteTimeoutMs);
|
|
|
|
|
}
|
2026-09-15 18:13:46 +07:00
|
|
|
}
|