Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ad522ef
Created new manager files.
Armored-Dragon Apr 13, 2026
f4ff364
Created base server scene.
Armored-Dragon Apr 13, 2026
a42ff7d
Start multiplayer server.
Armored-Dragon Apr 13, 2026
257f08e
Fixed dashboard error.
Armored-Dragon Apr 13, 2026
06d4d91
Load scene root.
Armored-Dragon Apr 13, 2026
6fab2c0
Force spawn the host.
Armored-Dragon Apr 13, 2026
6c51bb0
Start server managers.
Armored-Dragon Apr 13, 2026
f3ec248
Added function name to logger.
Armored-Dragon Apr 13, 2026
f94108a
Removed old files.
Armored-Dragon Apr 13, 2026
7dce447
Fixed port finder bug.
Armored-Dragon Apr 14, 2026
44110db
Initial session advertising work.
Armored-Dragon Apr 14, 2026
c4637fc
Removed unneeded Enum.
Armored-Dragon Apr 14, 2026
57c104e
Initial heartbeat work.
Armored-Dragon Apr 14, 2026
900e677
Session Updating.
Armored-Dragon Apr 14, 2026
b8f293d
Session delisting.
Armored-Dragon Apr 14, 2026
923da57
Settings placeholder.
Armored-Dragon Apr 16, 2026
3c97479
Remove session server from config.
Armored-Dragon Apr 16, 2026
83be113
Add session servers.
Armored-Dragon Apr 16, 2026
4c55963
Don't add empty session servers.
Armored-Dragon Apr 16, 2026
beca15e
Hide session server dialog in settings.
Armored-Dragon Apr 16, 2026
c791da8
Use session server list to advertise sessions to.
Armored-Dragon Apr 16, 2026
d68fd6e
Error fixes.
Armored-Dragon Apr 16, 2026
2820ab1
Properly destroy master scene when creating a server failed.
Armored-Dragon Apr 16, 2026
b1a380a
Error handling.
Armored-Dragon Apr 16, 2026
c540eca
Stop scene when replacing root.
Armored-Dragon Apr 16, 2026
218871f
Removed hardcoded localhost values used for debugging and development.
Armored-Dragon Apr 16, 2026
d719590
Moved TODO to it's own issue.
Armored-Dragon Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ boot_splash/minimum_display_time=1000
LaunchArguments="*uid://c45jrfmrjtnyn"
GlobalLogger="*uid://dgmfafi41y1nk"
FileManager="*uid://d2s50p717g3n"
SettingsManager="*uid://bj4giyk05v042"
AccountServers="*uid://bpysjoq7n0ytu"
Random="*uid://1js68qt8w0mv"
GlobalAccount="*uid://dtlb70kxvbtvn"
Events="*uid://c656spc3ppdlw"
OAuth="*uid://jd7qlsley1no"
SessionQuery="*uid://bl3e1utjvw3ul"
Enum="*uid://b5amtwc5yahe3"
UrlParser="*uid://budprjmmpally"

[display]
Expand Down
14 changes: 14 additions & 0 deletions src/scenes/levels/base.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[gd_scene format=3 uid="uid://bysrhijnau31g"]

[ext_resource type="Script" uid="uid://c13l6ywhxq6n5" path="res://scenes/managers/scene/player.gd" id="1_c3hqj"]
[ext_resource type="Script" uid="uid://cxbdjoelope42" path="res://scenes/managers/scene/signalbus.gd" id="2_fundk"]

[node name="Base" type="Node3D" unique_id=2084163635]

[node name="PlayerManager" type="Node" parent="." unique_id=412138074]
script = ExtResource("1_c3hqj")

[node name="SignalBus" type="Node" parent="." unique_id=165926833]
script = ExtResource("2_fundk")

[node name="root" type="Node3D" parent="." unique_id=345927695]
354 changes: 354 additions & 0 deletions src/scenes/managers/app/network.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,354 @@
# --- License
# File: /client/src/scenes/managers/app/network.gd
# Project: OpenMinerva
# Created Date: 13 April 2026
# Copyright (c) 2026 OpenMinerva
# License: MIT License
# Authors: Armored Dragon
# --- License

extends Node

const MAX_CLIENTS = 1000

var n_c = preload("res://scripts/network/network_compression.gd").new()
var url_regex = RegEx.create_from_string("^(https?)://([^/:]+)(?::(\\d+))?(.*)$")

@onready var http = preload("res://scripts/network/http.gd").new()
@onready var scene_m = get_tree().current_scene.get_node("SceneManager")
@onready var rpc_lib = get_tree().current_scene.get_node("RpcManager")

var _database = {
"heartbeats": {},
"sessions_id": {},
"sessions": {},
"sessions_api": {}
}

const _instance_database_template = {
"id": "",
"name": "",
"description": "",
"port": 0,
"max_connected_users": 1,
"privacy": null,

"connected_players": [],
"start_time": 0,

"networking": {
"use_steam": false,
"use_lan": false
}
}

func start_server(port: int = 0, root_scene: Enum.BaseLevel = Enum.BaseLevel.GRID) -> Dictionary:
var response_dict = {"ok": false, "error": null, "data": null}

# Get an available port. If port was defined, force that port or fail.
if port == 0:
var port_available = !_is_port_in_use(port)
if !port_available:
response_dict.error = "Port is not available."
return response_dict
else:
port = _find_available_port()

# Create server master scene.
var _scene: String = scene_m.create_master_scene()
var _instance = _instance_database_template.duplicate()
_instance.id = _scene
_instance.name = _scene
_instance.start_time = int(Time.get_unix_time_from_system())
_instance.privacy = Enum.PrivacyLevel.INVITE

# Create a new peer.
var _mp_api = SceneMultiplayer.new()
var _session_peer = ENetMultiplayerPeer.new()
var _create_server_response = _session_peer.create_server(port, MAX_CLIENTS)
_mp_api.multiplayer_peer = _session_peer

_database.sessions_api.set(_scene, _mp_api)
_database.sessions.set(_scene, _instance)

if _create_server_response != OK:
GlobalLogger.logs("Failed to start server. Error: '%s'" % _create_server_response, 1)
response_dict.error = str(_create_server_response)

_database.sessions_api.erase(_scene)
_database.sessions.erase(_scene)

scene_m.destroy_master_scene(_scene)
return response_dict

# Create server root scene.
if root_scene:
scene_m.set_master_root_from_program(_scene, root_scene)
else:
scene_m.set_master_root_from_program(_scene, Enum.BaseLevel.GRID)

scene_m.start_master_scene(_scene)

# DEV: Force spawn the host.
scene_m.get_master_scene(_scene).get_node("PlayerManager").spawn_player(1)

return response_dict

func stop_server(_id: String):
GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], 3)
# Kick all players (Server closing).
# Turn off all join requests.
# Destroy multiplayer api.
# Stop all managers.
# Destroy server master scene.
return

func update_server(id: String, server_info: Dictionary):
# Get server from database.
# Validate server updated data.
# Update the database entry.
# Emit server updated event to the server.
var _saved_session_servers = SettingsManager.get_session_servers()

if server_info.privacy > Enum.PrivacyLevel.INVITE:
for _server in _saved_session_servers:
if _database.heartbeats.has(id):
GlobalLogger.logs("Session '%s' is already advertised. Updating instead." % id)
await _update_session_server_listing(server_info, _server.url)
else:
var advertise_response = await _advertise_session(server_info, _server.url)

if advertise_response.ok == true:
_database.sessions_id.set(server_info.id, advertise_response.data.id)
_create_heartbeat_timer(server_info.id, _server.url)

if server_info.privacy == Enum.PrivacyLevel.INVITE:
if _database.heartbeats.has(id):
GlobalLogger.logs("Destroying session heartbeat for '%s'" % id)
_database.heartbeats.erase(id)

for _server in _saved_session_servers:
_remove_session_from_server(id, _server.url)
return

func join_server(_ip: String, _port: int):
GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], 3)
# Create multiplayer peer.
# Establish connection.
# Host handles everything after this with the managers?
return

func leave_server():
GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], 3)
# Get server from database.
# Send leave packet.
# Destroy multiplayer API.
# Destroy server master scene.
return

func kick_player():
GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], 3)
return

func ban_player():
GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], 3)
return

func on_kicked():
GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], 3)
return

func on_banned():
GlobalLogger.logs("'%s' is not implemented." % get_stack()[0]["function"], 3)
return

func get_connected_sessions():
var result = []

for session_id in _database.sessions.keys():
result.append(_database.sessions[session_id].merged({"id": session_id}))

return result

func _update_session_server_listing(session_info: Dictionary, session_server: String) -> Dictionary:
var response_dict = {"ok": false, "error": null, "data": null}

GlobalLogger.logs("Updating session '%s' to the server '%s'" % [session_info.id, session_server])
var url = UrlParser.deconstruct("%s/api/v1/updateSession" % session_server)

if url.ok != true:
GlobalLogger.logs("Failed to deconstruct the URL '%s'. Error: '%s'" % [session_server, url.error])
response_dict.error = url.error
return response_dict

url = url.data

var _body = {
"id": _database.sessions_id.get(session_info.id),
"session_name": session_info.name,
"session_description": session_info.description,
"session_privacy": session_info.privacy,
}

var _update_response = await http.req(
HTTPClient.Method.METHOD_POST,
url.host,
url.path,
url.port,
["Accept: application/json", "Content-Type: application/json", "x-api-key: %s" % GlobalAccount.dev_session_server_api_key],
JSON.stringify(_body)
)

return response_dict

func _remove_session_from_server(server_id: String, session_server: String) -> Dictionary:
var response_dict = {"ok": false, "error": null, "data": {}}
var _full_url = "%s/api/v1/deleteSession" % session_server

GlobalLogger.logs("Removing session '%s' to the server '%s'" % [server_id, session_server])
var url = UrlParser.deconstruct(_full_url)

if url.ok != true:
GlobalLogger.logs("Failed to deconstruct the URL '%s'. Error: '%s'" % [_full_url, url.error])
response_dict.error = url.error
return response_dict

url = url.data

var _body = {
"id": _database.sessions_id.get(server_id),
}

var _removal_response = await http.req(
HTTPClient.Method.METHOD_DELETE,
url.host,
url.path,
url.port,
["Accept: application/json", "Content-Type: application/json", "x-api-key: %s" % GlobalAccount.dev_session_server_api_key],
JSON.stringify(_body)
)

if _removal_response.ok:
response_dict.ok = true
response_dict.data = JSON.parse_string(_removal_response.body)
return response_dict

response_dict.error = _removal_response.error
return response_dict

func _find_available_port(target_port: int = 20205) -> int:
GlobalLogger.logs("Trying to find an available port starting at '%s'." % target_port)
var _found_port = null
var _is_found = false

while _is_found == false:
var port_available = !_is_port_in_use(target_port)
if port_available:
_found_port = target_port
_is_found = true

target_port = target_port + 1

GlobalLogger.logs("Port found: '%s'" % target_port)

return _found_port

func _is_port_in_use(port: int) -> bool:
var tcp_server = TCPServer.new()
var err = tcp_server.listen(port, "*")

if err == OK:
tcp_server.stop()
return false

return true

func _create_heartbeat_timer(session_id: String, session_server_url: String):
GlobalLogger.logs("Creating a heartbeat timer for server '%s'" % session_id)
# FIXME: Hardcoded time for timer.
var timer = get_tree().create_timer(20)

_database.heartbeats[session_id] = timer

timer.timeout.connect(_heartbeat_timer_timeout.bind(session_id, session_server_url))
return

func _heartbeat_timer_timeout(session_id: String, session_server_url: String):
GlobalLogger.logs("Sending a heartbeat for server '%s'" % session_id)
if _database.heartbeats.has(session_id) == false:
GlobalLogger.logs("Server '%s' does not exist anymore, not sending a heartbeat." % session_id)
return

_heartbeat_session(session_id, session_server_url)

_create_heartbeat_timer(session_id, session_server_url)
return

func _heartbeat_session(session_id: String, session_server_url: String) -> void:
var _full_url = "%s/api/v1/heartbeatSession" % session_server_url
var _url = UrlParser.deconstruct(_full_url)

if _url.ok != true:
GlobalLogger.logs("Failed to deconstruct the URL '%s'. Error: '%s'" % [_full_url, _url.error])
return

_url = _url.data
var body = {"session_id": session_id}

var response = await http.req(
HTTPClient.Method.METHOD_POST,
_url.host,
_url.path,
_url.port,
["Accept: application/json", "Content-Type: application/json", "x-api-key: %s" % GlobalAccount.dev_session_server_api_key],
JSON.stringify(body)
)

if response and response.get("ok"):
GlobalLogger.logs("Heartbeat sent for session '%s'" % session_id, 0)
return

func _advertise_session(session_info: Dictionary, session_server: String) -> Dictionary:
var response_dict = {"ok": false, "error": null, "data": null}
GlobalLogger.logs("Advertising session '%s' to the server '%s'" % [session_info.id, session_server])
var _full_url = "%s/api/v1/postSession" % session_server
var url = UrlParser.deconstruct(_full_url)

if url.ok != true:
GlobalLogger.logs("Failed to deconstruct the URL '%s'. Error: '%s'" % [_full_url, url.error])
response_dict.error = url.error
return response_dict

url = url.data

var _body = {
"session_name": session_info.name,
"session_description": session_info.description,
"session_privacy": session_info.privacy,
}

var advertise_response = await http.req(
HTTPClient.Method.METHOD_POST,
url.host,
url.path,
url.port,
["Accept: application/json", "Content-Type: application/json", "x-api-key: %s" % GlobalAccount.dev_session_server_api_key],
JSON.stringify(_body)
)


# FIXME: What is this flow? This is bad?
if advertise_response.ok == false:
response_dict.error = advertise_response.error
return response_dict

advertise_response = JSON.parse_string(advertise_response.body)
if advertise_response.ok == false:
response_dict.error = advertise_response.error
return response_dict

advertise_response = advertise_response.data

response_dict.ok = true
response_dict.data = advertise_response
return response_dict
1 change: 1 addition & 0 deletions src/scenes/managers/app/network.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://r51flk0bjypx
6 changes: 6 additions & 0 deletions src/scenes/managers/app/network.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[gd_scene format=3 uid="uid://5v8rbnp716b0"]

[ext_resource type="Script" uid="uid://r51flk0bjypx" path="res://scenes/managers/app/network.gd" id="1_daels"]

[node name="NetworkManager" type="Node"]
script = ExtResource("1_daels")
Loading