mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-26 22:06:42 +01:00
Merge pull request #170 from wxndr/feature/dedicated-server-gui
Server: add native DirectX 12 graphical server GUI & live telemetry
This commit is contained in:
10 files changed
+4573
-39
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#ifdef small
|
||||
#undef small
|
||||
#endif
|
||||
#include <imgui.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dingosdk::server_gui {
|
||||
|
||||
struct GuiFonts {
|
||||
ImFont* body{nullptr};
|
||||
ImFont* caption{nullptr};
|
||||
ImFont* bold{nullptr};
|
||||
ImFont* heading{nullptr};
|
||||
ImFont* title{nullptr};
|
||||
ImFont* mono{nullptr};
|
||||
};
|
||||
|
||||
enum class Tab {
|
||||
Console,
|
||||
Overview,
|
||||
Players,
|
||||
World,
|
||||
Moderation,
|
||||
Telemetry,
|
||||
Settings,
|
||||
Count
|
||||
};
|
||||
|
||||
struct ToastNotification {
|
||||
std::string message;
|
||||
double feedback_until{0.0};
|
||||
ImU32 accent{0};
|
||||
};
|
||||
|
||||
struct GuiState {
|
||||
Tab current_tab{Tab::Console};
|
||||
int moderation_subtab{0};
|
||||
|
||||
// Console state
|
||||
char console_input[512]{};
|
||||
std::vector<std::string> command_history;
|
||||
int history_index{-1};
|
||||
bool auto_scroll{true};
|
||||
std::string console_filter;
|
||||
|
||||
// Moderation form
|
||||
char mod_steam_id[64]{};
|
||||
int mod_type_index{0}; // 0 = Voice, 1 = Text, 2 = Both, 3 = Ban
|
||||
int mod_duration{60}; // minutes
|
||||
char mod_reason[128]{"Violating server guidelines"};
|
||||
|
||||
// World & Map
|
||||
char custom_map[128]{};
|
||||
|
||||
// Players Search Filter
|
||||
char player_search[64]{};
|
||||
|
||||
// Telemetry & Performance Monitor state
|
||||
int telemetry_time_scale{1}; // 0 = 60s, 1 = 5m, 2 = 15m, 3 = 1h
|
||||
int telemetry_metric{0}; // 0 = TPS & Tick Health, 1 = Latency & Frame Time, 2 = Memory & Heap
|
||||
|
||||
// Moderation Confirmation Modal State
|
||||
bool show_player_modal{false};
|
||||
bool is_kick_modal{false};
|
||||
std::uint64_t target_player_id{0};
|
||||
std::string target_player_name;
|
||||
char target_action_reason[128]{"Violating server guidelines"};
|
||||
|
||||
// Notification feedback banner & stacking queue
|
||||
std::string feedback_text;
|
||||
double feedback_until{0.0};
|
||||
std::vector<ToastNotification> notifications;
|
||||
|
||||
// Copy notice
|
||||
double copied_until{0.0};
|
||||
};
|
||||
|
||||
int run_gui(HINSTANCE instance, int cmd_show);
|
||||
|
||||
} // namespace dingosdk::server_gui
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
#include "server_gui_renderer.h"
|
||||
|
||||
#include <dxgi1_6.h>
|
||||
#include <backends/imgui_impl_dx12.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
namespace dingosdk::server_gui {
|
||||
|
||||
bool Renderer::init(HWND window) {
|
||||
UINT factory_flags = 0;
|
||||
ComPtr<IDXGIFactory4> factory;
|
||||
if (FAILED(CreateDXGIFactory2(factory_flags, IID_PPV_ARGS(&factory)))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try high performance GPU first
|
||||
if (ComPtr<IDXGIFactory6> ranked; SUCCEEDED(factory.As(&ranked))) {
|
||||
for (UINT index = 0; !device_; ++index) {
|
||||
ComPtr<IDXGIAdapter1> candidate;
|
||||
if (FAILED(ranked->EnumAdapterByGpuPreference(index, DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE,
|
||||
IID_PPV_ARGS(&candidate)))) break;
|
||||
DXGI_ADAPTER_DESC1 description{};
|
||||
if (FAILED(candidate->GetDesc1(&description)) || (description.Flags & DXGI_ADAPTER_FLAG_SOFTWARE))
|
||||
continue;
|
||||
D3D12CreateDevice(candidate.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_));
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default device or WARP software device
|
||||
if (!device_ && FAILED(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_)))) {
|
||||
ComPtr<IDXGIAdapter> warp;
|
||||
if (FAILED(factory->EnumWarpAdapter(IID_PPV_ARGS(&warp))) ||
|
||||
FAILED(D3D12CreateDevice(warp.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_)))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
D3D12_DESCRIPTOR_HEAP_DESC rtv_desc{D3D12_DESCRIPTOR_HEAP_TYPE_RTV, frame_count, D3D12_DESCRIPTOR_HEAP_FLAG_NONE, 1};
|
||||
D3D12_DESCRIPTOR_HEAP_DESC srv_desc{D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, 64,
|
||||
D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE, 0};
|
||||
D3D12_COMMAND_QUEUE_DESC queue_desc{D3D12_COMMAND_LIST_TYPE_DIRECT, 0, D3D12_COMMAND_QUEUE_FLAG_NONE, 1};
|
||||
|
||||
if (FAILED(device_->CreateDescriptorHeap(&rtv_desc, IID_PPV_ARGS(&rtv_heap_))) ||
|
||||
FAILED(device_->CreateDescriptorHeap(&srv_desc, IID_PPV_ARGS(&srv_heap_))) ||
|
||||
FAILED(device_->CreateCommandQueue(&queue_desc, IID_PPV_ARGS(&queue_)))) return false;
|
||||
|
||||
for (auto& frame : frames_) {
|
||||
if (FAILED(device_->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&frame.allocator)))) return false;
|
||||
}
|
||||
|
||||
if (FAILED(device_->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, frames_[0].allocator.Get(), nullptr,
|
||||
IID_PPV_ARGS(&list_))) || FAILED(list_->Close()) ||
|
||||
FAILED(device_->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence_)))) return false;
|
||||
|
||||
fence_event_ = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
||||
if (!fence_event_) return false;
|
||||
|
||||
DXGI_SWAP_CHAIN_DESC1 swap_desc{};
|
||||
swap_desc.BufferCount = frame_count;
|
||||
swap_desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
swap_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
|
||||
swap_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
|
||||
swap_desc.SampleDesc.Count = 1;
|
||||
swap_desc.AlphaMode = DXGI_ALPHA_MODE_IGNORE;
|
||||
swap_desc.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT;
|
||||
|
||||
ComPtr<IDXGISwapChain1> swap;
|
||||
if (FAILED(factory->CreateSwapChainForHwnd(queue_.Get(), window, &swap_desc, nullptr, nullptr, &swap)) ||
|
||||
FAILED(swap.As(&swap_))) return false;
|
||||
|
||||
factory->MakeWindowAssociation(window, DXGI_MWA_NO_ALT_ENTER);
|
||||
swap_->SetMaximumFrameLatency(frame_count);
|
||||
waitable_ = swap_->GetFrameLatencyWaitableObject();
|
||||
|
||||
const auto rtv_size = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
|
||||
auto rtv = rtv_heap_->GetCPUDescriptorHandleForHeapStart();
|
||||
for (UINT index = 0; index < frame_count; ++index) {
|
||||
targets_[index].handle = rtv;
|
||||
if (FAILED(swap_->GetBuffer(index, IID_PPV_ARGS(&targets_[index].resource)))) return false;
|
||||
device_->CreateRenderTargetView(targets_[index].resource.Get(), nullptr, rtv);
|
||||
rtv.ptr += rtv_size;
|
||||
}
|
||||
|
||||
ImGui_ImplDX12_InitInfo info;
|
||||
info.Device = device_.Get();
|
||||
info.CommandQueue = queue_.Get();
|
||||
info.NumFramesInFlight = frame_count;
|
||||
info.RTVFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
info.DSVFormat = DXGI_FORMAT_UNKNOWN;
|
||||
info.SrvDescriptorHeap = srv_heap_.Get();
|
||||
info.LegacySingleSrvCpuDescriptor = srv_heap_->GetCPUDescriptorHandleForHeapStart();
|
||||
info.LegacySingleSrvGpuDescriptor = srv_heap_->GetGPUDescriptorHandleForHeapStart();
|
||||
|
||||
if (!ImGui_ImplDX12_Init(&info)) {
|
||||
return false;
|
||||
}
|
||||
if (!ImGui_ImplDX12_CreateDeviceObjects()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Renderer::render() {
|
||||
auto& frame = frames_[frame_index_ % frame_count];
|
||||
wait(frame.fence_value);
|
||||
if (waitable_) WaitForSingleObject(waitable_, 1000);
|
||||
|
||||
const auto back = swap_->GetCurrentBackBufferIndex();
|
||||
frame.allocator->Reset();
|
||||
list_->Reset(frame.allocator.Get(), nullptr);
|
||||
|
||||
D3D12_RESOURCE_BARRIER barrier{};
|
||||
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
|
||||
barrier.Transition.pResource = targets_[back].resource.Get();
|
||||
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
|
||||
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
|
||||
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
|
||||
list_->ResourceBarrier(1, &barrier);
|
||||
|
||||
// Dark slate background color matching ReSkate Trainer / Launcher
|
||||
constexpr float clear[4]{0.035f, 0.043f, 0.059f, 1.0f};
|
||||
list_->ClearRenderTargetView(targets_[back].handle, clear, 0, nullptr);
|
||||
list_->OMSetRenderTargets(1, &targets_[back].handle, FALSE, nullptr);
|
||||
|
||||
ID3D12DescriptorHeap* heaps[]{srv_heap_.Get()};
|
||||
list_->SetDescriptorHeaps(1, heaps);
|
||||
|
||||
ImGui_ImplDX12_RenderDrawData(ImGui::GetDrawData(), list_.Get());
|
||||
|
||||
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
|
||||
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
|
||||
list_->ResourceBarrier(1, &barrier);
|
||||
list_->Close();
|
||||
|
||||
ID3D12CommandList* lists[]{list_.Get()};
|
||||
queue_->ExecuteCommandLists(1, lists);
|
||||
swap_->Present(1, 0); // VSync enabled for smooth UI
|
||||
|
||||
frame.fence_value = ++fence_value_;
|
||||
queue_->Signal(fence_.Get(), frame.fence_value);
|
||||
++frame_index_;
|
||||
}
|
||||
|
||||
void Renderer::resize(UINT width, UINT height) {
|
||||
if (!swap_ || width == 0 || height == 0) return;
|
||||
|
||||
for (auto& frame : frames_) wait(frame.fence_value);
|
||||
for (auto& target : targets_) target.resource.Reset();
|
||||
|
||||
swap_->ResizeBuffers(frame_count, width, height, DXGI_FORMAT_R8G8B8A8_UNORM,
|
||||
DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT);
|
||||
|
||||
const auto rtv_size = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
|
||||
auto rtv = rtv_heap_->GetCPUDescriptorHandleForHeapStart();
|
||||
for (UINT index = 0; index < frame_count; ++index) {
|
||||
targets_[index].handle = rtv;
|
||||
swap_->GetBuffer(index, IID_PPV_ARGS(&targets_[index].resource));
|
||||
device_->CreateRenderTargetView(targets_[index].resource.Get(), nullptr, rtv);
|
||||
rtv.ptr += rtv_size;
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::wait(UINT64 value) {
|
||||
if (fence_->GetCompletedValue() >= value) return;
|
||||
fence_->SetEventOnCompletion(value, fence_event_);
|
||||
WaitForSingleObject(fence_event_, INFINITE);
|
||||
}
|
||||
|
||||
void Renderer::shutdown() {
|
||||
for (auto& frame : frames_) wait(frame.fence_value);
|
||||
ImGui_ImplDX12_Shutdown();
|
||||
for (auto& target : targets_) target.resource.Reset();
|
||||
if (swap_) swap_.Reset();
|
||||
if (waitable_) { CloseHandle(waitable_); waitable_ = nullptr; }
|
||||
if (fence_event_) { CloseHandle(fence_event_); fence_event_ = nullptr; }
|
||||
fence_.Reset();
|
||||
list_.Reset();
|
||||
for (auto& frame : frames_) frame.allocator.Reset();
|
||||
queue_.Reset();
|
||||
srv_heap_.Reset();
|
||||
rtv_heap_.Reset();
|
||||
device_.Reset();
|
||||
}
|
||||
|
||||
} // namespace dingosdk::server_gui
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#include <d3d12.h>
|
||||
#include <dxgi1_4.h>
|
||||
#include <wrl/client.h>
|
||||
#include <imgui.h>
|
||||
#ifdef small
|
||||
#undef small
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace dingosdk::server_gui {
|
||||
|
||||
using Microsoft::WRL::ComPtr;
|
||||
|
||||
constexpr UINT frame_count = 2;
|
||||
|
||||
class Renderer {
|
||||
public:
|
||||
bool init(HWND window);
|
||||
void render();
|
||||
void resize(UINT width, UINT height);
|
||||
void shutdown();
|
||||
|
||||
private:
|
||||
struct Frame {
|
||||
ComPtr<ID3D12CommandAllocator> allocator;
|
||||
UINT64 fence_value{};
|
||||
};
|
||||
struct Target {
|
||||
ComPtr<ID3D12Resource> resource;
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE handle{};
|
||||
};
|
||||
|
||||
ComPtr<ID3D12Device> device_;
|
||||
ComPtr<ID3D12DescriptorHeap> rtv_heap_, srv_heap_;
|
||||
ComPtr<ID3D12CommandQueue> queue_;
|
||||
ComPtr<ID3D12GraphicsCommandList> list_;
|
||||
ComPtr<ID3D12Fence> fence_;
|
||||
ComPtr<IDXGISwapChain3> swap_;
|
||||
std::array<Frame, frame_count> frames_;
|
||||
std::array<Target, frame_count> targets_;
|
||||
HANDLE fence_event_{};
|
||||
HANDLE waitable_{};
|
||||
UINT64 fence_value_{};
|
||||
UINT64 frame_index_{};
|
||||
|
||||
void wait(UINT64 value);
|
||||
};
|
||||
|
||||
} // namespace dingosdk::server_gui
|
||||
|
||||
+76
-23
@@ -103,17 +103,27 @@ Console &console() {
|
||||
}
|
||||
void write_log(const std::string &text) {
|
||||
std::lock_guard lock(log_mutex);
|
||||
const auto now = std::time(nullptr);
|
||||
std::tm local{};
|
||||
char stamp[64]{};
|
||||
char file_stamp[64]{};
|
||||
#ifdef _WIN32
|
||||
localtime_s(&local, &now);
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
if (GetTimeFormatA(LOCALE_USER_DEFAULT, 0, &st, nullptr, stamp, sizeof(stamp)) == 0) {
|
||||
const bool pm = st.wHour >= 12;
|
||||
const int hour12 = (st.wHour % 12 == 0) ? 12 : (st.wHour % 12);
|
||||
std::snprintf(stamp, sizeof(stamp), "%d:%02d:%02d %s", hour12, st.wMinute, st.wSecond, pm ? "PM" : "AM");
|
||||
}
|
||||
std::snprintf(file_stamp, sizeof(file_stamp), "%04d-%02d-%02d %s", st.wYear, st.wMonth, st.wDay, stamp);
|
||||
#else
|
||||
localtime_r(&now, &local);
|
||||
const auto now_tp = std::chrono::system_clock::now();
|
||||
const auto now_t = std::chrono::system_clock::to_time_t(now_tp);
|
||||
std::tm local{};
|
||||
localtime_r(&now_t, &local);
|
||||
std::strftime(stamp, sizeof(stamp), "%X", &local);
|
||||
std::strftime(file_stamp, sizeof(file_stamp), "%Y-%m-%d %H:%M:%S", &local);
|
||||
#endif
|
||||
char stamp[32]{};
|
||||
std::strftime(stamp, sizeof stamp, "%Y-%m-%d %H:%M:%S", &local);
|
||||
if (log_file) log_file << '[' << stamp << "] " << text << std::endl;
|
||||
console().write(std::string("[") + (stamp + 11) + "] " + text + "\n");
|
||||
if (log_file) log_file << '[' << file_stamp << "] " << text << std::endl;
|
||||
console().write(std::string("[") + stamp + "] " + text + "\n");
|
||||
}
|
||||
std::atomic<bool> finished{};
|
||||
#ifdef _WIN32
|
||||
@@ -249,6 +259,14 @@ int run(int argc, wchar_t **argv, bool skip_update) {
|
||||
setvbuf(stdout, nullptr, _IONBF, 0);
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
SetConsoleCP(CP_UTF8);
|
||||
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
if (hOut != INVALID_HANDLE_VALUE) {
|
||||
DWORD dwMode = 0;
|
||||
if (GetConsoleMode(hOut, &dwMode)) {
|
||||
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
||||
SetConsoleMode(hOut, dwMode);
|
||||
}
|
||||
}
|
||||
SetConsoleCtrlHandler(on_console, TRUE);
|
||||
const auto here = folder();
|
||||
// Release builds: ReSkateServer --export-world-layers "<Skate folder>" "<file>" writes the
|
||||
@@ -484,8 +502,7 @@ int run(int argc, char **argv, bool skip_update) {
|
||||
std::future<UpdateCheck> update_check;
|
||||
bool update_now{}, update_waiting{}, restart{};
|
||||
// The backend's ban list (global_bans.h): read now and every ten minutes, a minute after a
|
||||
// failure. "global_bans": false lets those players in; the answer is still read, for the
|
||||
// chat word lists that come in it.
|
||||
// failure. "global_bans": false leaves it unread and lets those players in.
|
||||
std::future<BanListCheck> ban_check;
|
||||
auto next_ban_check = next_advertise;
|
||||
bool bans_unread{};
|
||||
@@ -572,7 +589,7 @@ int run(int argc, char **argv, bool skip_update) {
|
||||
if (check.ok && check.words_changed)
|
||||
write_log("Word lists: " + std::to_string(check.filtered_words) + " filtered and " + std::to_string(check.forbidden_words) +
|
||||
" not allowed at all, from the ReSkate team's lists." +
|
||||
(config.word_warnings ? "" : " (\"word_warnings\" is 0: a message with one is not passed on, and nobody is warned or kicked.)"));
|
||||
(config.word_warnings ? "" : " (\"word_warnings\" is 0: a message with one is not passed on, and nobody is kicked)."));
|
||||
else if (!check.ok && !bans_unread)
|
||||
write_log("The ReSkate team's lists (global bans and chat words) could not be read (" + check.problem +
|
||||
"). Trying again every minute; until then the ones already read hold.");
|
||||
@@ -654,19 +671,55 @@ int serve(int argc, auto **argv) {
|
||||
} // namespace
|
||||
|
||||
#ifdef _WIN32
|
||||
int wmain(int argc, wchar_t **argv) {
|
||||
const int code = serve(argc, argv);
|
||||
finished = true;
|
||||
// Keep a failure on screen until the host has read it, instead of the window
|
||||
// vanishing with it. Closing the window or Ctrl+C still ends it at once.
|
||||
if (code != 0 && !stopping && own_window()) {
|
||||
auto &input = console_input();
|
||||
input.take();
|
||||
console().write("Press Enter to close.\n");
|
||||
while (!stopping && input.take().empty()) Sleep(50);
|
||||
#include "GUI/server_gui.h"
|
||||
#include <shellapi.h>
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int cmd_show) {
|
||||
int argc = 0;
|
||||
LPWSTR *argvW = CommandLineToArgvW(GetCommandLineW(), &argc);
|
||||
bool headless = false;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (!argvW[i]) continue;
|
||||
std::wstring arg = argvW[i];
|
||||
if (arg == L"--headless" || arg == L"--cli" || arg == L"--no-gui" || arg == L"-nogui" || arg == L"-headless" || arg == L"-cli") {
|
||||
headless = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (headless) {
|
||||
if (AttachConsole(ATTACH_PARENT_PROCESS) || AllocConsole()) {
|
||||
FILE *fp = nullptr;
|
||||
freopen_s(&fp, "CONOUT$", "w", stdout);
|
||||
freopen_s(&fp, "CONOUT$", "w", stderr);
|
||||
freopen_s(&fp, "CONIN$", "r", stdin);
|
||||
std::ios::sync_with_stdio(true);
|
||||
}
|
||||
const int code = serve(argc, argvW);
|
||||
LocalFree(argvW);
|
||||
finished = true;
|
||||
if (code != 0 && !stopping && own_window()) {
|
||||
auto &input = console_input();
|
||||
input.take();
|
||||
console().write("Press Enter to close.\n");
|
||||
while (!stopping && input.take().empty()) Sleep(50);
|
||||
}
|
||||
console().flush(std::chrono::seconds(2));
|
||||
return code;
|
||||
}
|
||||
|
||||
LocalFree(argvW);
|
||||
try {
|
||||
return dingosdk::server_gui::run_gui(instance, cmd_show);
|
||||
} catch (const std::exception &e) {
|
||||
std::ofstream f("gui_exception.txt");
|
||||
f << "EXCEPTION: " << e.what() << "\n";
|
||||
return 1;
|
||||
} catch (...) {
|
||||
std::ofstream f("gui_exception.txt");
|
||||
f << "UNKNOWN EXCEPTION\n";
|
||||
return 1;
|
||||
}
|
||||
console().flush(std::chrono::seconds(2));
|
||||
return code;
|
||||
}
|
||||
#else
|
||||
int main(int argc, char **argv) {
|
||||
|
||||
@@ -0,0 +1,798 @@
|
||||
#include "server_engine.h"
|
||||
#include "global_bans.h"
|
||||
#include "server_update.h"
|
||||
#include "Extension/Multiplayer/Session/monotonic_clock.h"
|
||||
#include "Engine/Core/Text/word_filter.h"
|
||||
#include "Engine/Core/Platform/path_text.h"
|
||||
#include "Extension/Multiplayer/developer_identity.h"
|
||||
#include "Engine/Game/World/world_layer_catalog.h"
|
||||
#include "Engine/Game/World/world_names.h"
|
||||
#include "Engine/Vfs/world_layer_scan.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <Windows.h>
|
||||
#include <timeapi.h>
|
||||
#include <psapi.h>
|
||||
#include <tlhelp32.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <iostream>
|
||||
|
||||
namespace dingosdk::server {
|
||||
|
||||
ServerEngine& ServerEngine::instance() {
|
||||
static ServerEngine engine;
|
||||
return engine;
|
||||
}
|
||||
|
||||
ServerEngine::ServerEngine() = default;
|
||||
|
||||
ServerEngine::~ServerEngine() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void ServerEngine::log(std::string text, LogLevel level) {
|
||||
char stamp[64]{};
|
||||
#ifdef _WIN32
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
if (GetTimeFormatA(LOCALE_USER_DEFAULT, 0, &st, nullptr, stamp, sizeof(stamp)) == 0) {
|
||||
const bool pm = st.wHour >= 12;
|
||||
const int hour12 = (st.wHour % 12 == 0) ? 12 : (st.wHour % 12);
|
||||
std::snprintf(stamp, sizeof(stamp), "%d:%02d:%02d %s", hour12, st.wMinute, st.wSecond, pm ? "PM" : "AM");
|
||||
}
|
||||
#else
|
||||
const auto now_tp = std::chrono::system_clock::now();
|
||||
const auto now_t = std::chrono::system_clock::to_time_t(now_tp);
|
||||
std::tm local{};
|
||||
localtime_r(&now_t, &local);
|
||||
std::strftime(stamp, sizeof(stamp), "%X", &local);
|
||||
#endif
|
||||
|
||||
// Auto-detect log level if default info was provided
|
||||
if (level == LogLevel::info) {
|
||||
if (text.find("[chat]") != std::string::npos) {
|
||||
level = LogLevel::chat;
|
||||
} else if (text.find("[cmd]") != std::string::npos || text.find("[command]") != std::string::npos ||
|
||||
text.find("[admin]") != std::string::npos) {
|
||||
level = LogLevel::command;
|
||||
} else if (text.find("[+]") != std::string::npos || text.find(" joined (") != std::string::npos ||
|
||||
text.find(" is up on ") != std::string::npos || text.find("Join code:") != std::string::npos ||
|
||||
text.find("Update applied successfully") != std::string::npos) {
|
||||
level = LogLevel::success;
|
||||
} else if (text.find("[-]") != std::string::npos || text.find(" left (") != std::string::npos ||
|
||||
text.find(" was kicked") != std::string::npos || text.find(" was removed") != std::string::npos) {
|
||||
level = LogLevel::warning;
|
||||
} else if (text.starts_with("[error]") || text.starts_with("Error:") || text.starts_with("FATAL:") ||
|
||||
text.starts_with("Server error:") || text.starts_with("Command error:") ||
|
||||
text.find("Cannot read") != std::string::npos || text.find("error:") != std::string::npos) {
|
||||
level = LogLevel::error;
|
||||
} else if (text.starts_with("[warning]") || text.starts_with("Warning:") ||
|
||||
text.find("Mods: skipped") != std::string::npos || text.find("[network] The server is behind") != std::string::npos) {
|
||||
level = LogLevel::warning;
|
||||
}
|
||||
}
|
||||
|
||||
ServerLogEntry entry;
|
||||
entry.timestamp = stamp;
|
||||
entry.text = text;
|
||||
entry.level = level;
|
||||
|
||||
std::vector<LogCallback> callbacks;
|
||||
{
|
||||
std::lock_guard lock(log_mutex_);
|
||||
entry.id = next_log_id_++;
|
||||
if (log_history_.size() >= 2048) {
|
||||
log_history_.pop_front();
|
||||
}
|
||||
log_history_.push_back(entry);
|
||||
if (log_file_.is_open()) {
|
||||
log_file_ << "[" << entry.timestamp << "] " << text << std::endl;
|
||||
}
|
||||
callbacks = log_callbacks_;
|
||||
}
|
||||
|
||||
std::printf("[%s] %s\n", stamp, text.c_str());
|
||||
|
||||
for (const auto& cb : callbacks) {
|
||||
if (cb) cb(entry);
|
||||
}
|
||||
}
|
||||
|
||||
void ServerEngine::add_log_callback(LogCallback cb) {
|
||||
std::lock_guard lock(log_mutex_);
|
||||
log_callbacks_.push_back(std::move(cb));
|
||||
}
|
||||
|
||||
std::vector<ServerLogEntry> ServerEngine::log_entries(std::size_t from_id) const {
|
||||
std::lock_guard lock(log_mutex_);
|
||||
std::vector<ServerLogEntry> result;
|
||||
result.reserve(log_history_.size());
|
||||
for (const auto& item : log_history_) {
|
||||
if (item.id >= from_id) {
|
||||
result.push_back(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string ServerEngine::last_error() const {
|
||||
std::lock_guard lock(error_mutex_);
|
||||
return last_error_;
|
||||
}
|
||||
|
||||
void ServerEngine::clear_error() {
|
||||
std::lock_guard lock(error_mutex_);
|
||||
last_error_.clear();
|
||||
if (state_.load() == ServerState::Error) {
|
||||
state_.store(ServerState::Stopped);
|
||||
}
|
||||
}
|
||||
|
||||
bool ServerEngine::load_environment(const std::filesystem::path& folder, const std::filesystem::path& config_file, bool skip_update) {
|
||||
folder_ = folder;
|
||||
config_file_ = config_file;
|
||||
skip_update_ = skip_update;
|
||||
|
||||
if (!log_file_.is_open()) {
|
||||
log_file_.open(folder_ / "ReSkateServer.log", std::ios::app);
|
||||
}
|
||||
|
||||
try {
|
||||
const bool fresh = !std::filesystem::exists(config_file_);
|
||||
std::vector<std::string> added;
|
||||
config_ = load_config(config_file_, &added);
|
||||
if (fresh) log("Wrote default " + path_utf8(config_file_.filename()) + ". Edit it to name the server and add admins.");
|
||||
if (!added.empty()) {
|
||||
std::string names;
|
||||
for (const auto &name : added) names += (names.empty() ? "" : ", ") + name;
|
||||
log("Added new settings to " + path_utf8(config_file_.filename()) + ": " + names + ".");
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
std::string err = "Cannot read " + path_utf8(config_file_) + ": " + e.what();
|
||||
log(err, LogLevel::error);
|
||||
std::lock_guard lock(error_mutex_);
|
||||
last_error_ = err;
|
||||
state_.store(ServerState::Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto &problem : load_levels(folder_ / "Mods")) log("Mods: skipped " + problem, LogLevel::warning);
|
||||
if (levels().size() > 6) log("Mods: " + std::to_string(levels().size() - 6) + " custom map(s).", LogLevel::info);
|
||||
|
||||
bool renamed{};
|
||||
if (const auto setting = map_setting(config_.map); setting != config_.map && !setting.empty()) {
|
||||
config_.map = setting;
|
||||
renamed = true;
|
||||
}
|
||||
for (auto &map : config_.map_pool) {
|
||||
if (const auto *level = find_level(map); level && level->name != map) {
|
||||
map = level->name;
|
||||
renamed = true;
|
||||
}
|
||||
}
|
||||
if (renamed) try { save_config(config_); } catch (...) {}
|
||||
if (const auto error = config_error(config_); !error.empty()) {
|
||||
std::string err = "Config problem: " + error;
|
||||
log(err, LogLevel::error);
|
||||
std::lock_guard lock(error_mutex_);
|
||||
last_error_ = err;
|
||||
state_.store(ServerState::Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
remove_previous_update(folder_);
|
||||
|
||||
if (const auto catalog = folder_ / "world-layers.json"; std::filesystem::exists(catalog)) {
|
||||
try {
|
||||
install_world_layer_catalog(world_layer_scan::read(catalog));
|
||||
log("World layers: " + std::to_string(world_layers().size()) + " from world-layers.json.");
|
||||
} catch (const std::exception &e) {
|
||||
log(std::string("world-layers.json error: ") + e.what(), LogLevel::warning);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch ReSkate identity lists, categories (Centrix, Dev, Staff, Homie, Creator) and global bans in background
|
||||
std::thread([]() {
|
||||
try {
|
||||
read_global_bans();
|
||||
} catch (...) {}
|
||||
}).detach();
|
||||
|
||||
initialized_.store(true);
|
||||
state_.store(ServerState::Stopped);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ServerEngine::start_server_async() {
|
||||
auto current = state_.load();
|
||||
if (current == ServerState::Starting || current == ServerState::Running) return;
|
||||
|
||||
if (worker_thread_.joinable()) {
|
||||
worker_thread_.join();
|
||||
}
|
||||
if (start_thread_.joinable()) {
|
||||
start_thread_.join();
|
||||
}
|
||||
|
||||
clear_error();
|
||||
state_.store(ServerState::Starting);
|
||||
stopping_.store(false);
|
||||
|
||||
start_thread_ = std::thread([this] {
|
||||
run_startup();
|
||||
});
|
||||
}
|
||||
|
||||
void ServerEngine::run_startup() {
|
||||
try {
|
||||
log("Booting ReSkate dedicated server session...", LogLevel::info);
|
||||
|
||||
// Reload latest config before starting
|
||||
try {
|
||||
config_ = load_config(config_file_);
|
||||
} catch (...) {}
|
||||
|
||||
// If auto-update is configured and updates are enabled, check and apply update before opening ports
|
||||
if (config_.auto_update && updates_enabled()) {
|
||||
log("Checking for server updates...", LogLevel::info);
|
||||
const auto check = check_for_update();
|
||||
if (check.available) {
|
||||
log("New server release v" + check.version + " available. Installing update before startup...", LogLevel::info);
|
||||
try {
|
||||
install_update(folder_);
|
||||
log("Update applied successfully. Relaunching server...", LogLevel::success);
|
||||
if (relaunch()) {
|
||||
std::exit(0);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
log(std::string("Auto-update failed: ") + e.what() + ". Starting server with current version.", LogLevel::warning);
|
||||
}
|
||||
} else if (!check.problem.empty()) {
|
||||
log("Startup update check: " + check.problem, LogLevel::warning);
|
||||
} else {
|
||||
log("Server is up to date (v" + (check.version.empty() ? "2.0.3" : check.version) + ").", LogLevel::info);
|
||||
}
|
||||
}
|
||||
|
||||
steam_ = std::make_unique<SteamServer>();
|
||||
std::string error;
|
||||
if (!steam_->start(folder_, config_.port, config_.query_port, config_.steam_token, error)) {
|
||||
std::string err_text = "Steam server failed to start: " + error;
|
||||
log(err_text, LogLevel::error);
|
||||
std::lock_guard lock(error_mutex_);
|
||||
last_error_ = err_text;
|
||||
state_.store(ServerState::Error);
|
||||
cleanup_server_instances();
|
||||
return;
|
||||
}
|
||||
|
||||
log(config_.steam_token.empty() ? "Signing in to Steam..." : "Signing in to Steam with steam_token...");
|
||||
const auto login_started = std::chrono::steady_clock::now();
|
||||
while (!steam_->logged_on() && !stopping_) {
|
||||
steam_->run_callbacks();
|
||||
if (std::chrono::steady_clock::now() - login_started > std::chrono::seconds(45)) {
|
||||
std::string timeout_msg = config_.steam_token.empty()
|
||||
? "Steam sign-in timed out after 45 s. Check your internet connection."
|
||||
: "Steam sign-in timed out. Check steam_token validity or internet connection.";
|
||||
log(timeout_msg, LogLevel::error);
|
||||
std::lock_guard lock(error_mutex_);
|
||||
last_error_ = timeout_msg;
|
||||
state_.store(ServerState::Error);
|
||||
cleanup_server_instances();
|
||||
return;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
|
||||
if (stopping_) {
|
||||
cleanup_server_instances();
|
||||
state_.store(ServerState::Stopped);
|
||||
return;
|
||||
}
|
||||
|
||||
transport_ = std::make_unique<multiplayer::SteamTransport>();
|
||||
if (!transport_->open_game_server(steam_->module())) {
|
||||
std::string trans_err = "Steam networking failed: " + transport_->status().detail;
|
||||
log(trans_err, LogLevel::error);
|
||||
std::lock_guard lock(error_mutex_);
|
||||
last_error_ = trans_err;
|
||||
state_.store(ServerState::Error);
|
||||
cleanup_server_instances();
|
||||
return;
|
||||
}
|
||||
|
||||
host_ = std::make_unique<Host>(config_, *transport_, [this](const std::string& msg) {
|
||||
log(msg);
|
||||
});
|
||||
|
||||
if (!host_->start(error)) {
|
||||
std::string host_err = "Could not open server listener: " + error;
|
||||
log(host_err, LogLevel::error);
|
||||
std::lock_guard lock(error_mutex_);
|
||||
last_error_ = host_err;
|
||||
state_.store(ServerState::Error);
|
||||
cleanup_server_instances();
|
||||
return;
|
||||
}
|
||||
|
||||
log(config_.name + " is up on " + host_->map_name() + " for " + std::to_string(config_.max_players) + " players.", LogLevel::success);
|
||||
log("Steam ID " + std::to_string(steam_->steam_id()) +
|
||||
(config_.steam_token.empty() ? " (anonymous: new every start; set steam_token to keep one)" : " (from steam_token)") +
|
||||
", public IP " + steam_->public_ip() + ".", LogLevel::info);
|
||||
if (config_.steam_token.empty() && config_.listed) {
|
||||
log("No steam_token: the server browser can be set to show only servers that have one, and then "
|
||||
"this server is not in it (players can still join with the code). It takes a minute to make "
|
||||
"one: see steam_token in README.", LogLevel::warning);
|
||||
}
|
||||
log("Join code: " + host_->invite() + (config_.password.empty() ? "" : " (password required)"), LogLevel::success);
|
||||
log(config_.admins.empty() ? "No admins yet. Type 'admin add <SteamID64>' to add one."
|
||||
: std::to_string(config_.admins.size()) + " admin(s) configured.", LogLevel::info);
|
||||
|
||||
start_time_ = std::chrono::steady_clock::now();
|
||||
running_.store(true);
|
||||
state_.store(ServerState::Running);
|
||||
|
||||
worker_loop();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::string ex_msg = "Fatal Server Exception: " + std::string(e.what());
|
||||
log(ex_msg, LogLevel::error);
|
||||
std::lock_guard lock(error_mutex_);
|
||||
last_error_ = ex_msg;
|
||||
cleanup_server_instances();
|
||||
state_.store(ServerState::Error);
|
||||
} catch (...) {
|
||||
log("Unknown fatal server error during startup.", LogLevel::error);
|
||||
std::lock_guard lock(error_mutex_);
|
||||
last_error_ = "Unknown fatal exception in server startup.";
|
||||
cleanup_server_instances();
|
||||
state_.store(ServerState::Error);
|
||||
}
|
||||
}
|
||||
|
||||
void ServerEngine::cleanup_server_instances() {
|
||||
if (host_) {
|
||||
try { host_->stop("Server stopped."); } catch (...) {}
|
||||
host_.reset();
|
||||
}
|
||||
if (transport_) {
|
||||
try { transport_->stop(); } catch (...) {}
|
||||
transport_.reset();
|
||||
}
|
||||
if (steam_) {
|
||||
try { steam_->stop(); } catch (...) {}
|
||||
steam_.reset();
|
||||
}
|
||||
running_.store(false);
|
||||
}
|
||||
|
||||
void ServerEngine::worker_loop() {
|
||||
#ifdef _WIN32
|
||||
timeBeginPeriod(1);
|
||||
#endif
|
||||
auto next_advertise = std::chrono::steady_clock::now();
|
||||
auto next_update_check = std::chrono::steady_clock::now() + std::chrono::minutes(15);
|
||||
auto next_ban_check = std::chrono::steady_clock::now();
|
||||
std::future<BanListCheck> ban_check;
|
||||
bool bans_unread = true;
|
||||
std::optional<bool> name_allowed;
|
||||
const auto target_tick_interval = std::chrono::microseconds(1000000 / std::max(10U, config_.tps));
|
||||
|
||||
auto last_tick = std::chrono::steady_clock::now();
|
||||
auto fps_start = last_tick;
|
||||
size_t tick_counter = 0;
|
||||
bool tokens_required{};
|
||||
|
||||
while (!stopping_.load()) {
|
||||
const auto tick_start = std::chrono::steady_clock::now();
|
||||
steam_->run_callbacks();
|
||||
|
||||
try {
|
||||
host_->tick(multiplayer::now_us());
|
||||
} catch (const std::exception &e) {
|
||||
log(std::string("Server tick error: ") + e.what(), LogLevel::error);
|
||||
}
|
||||
|
||||
// Live ReSkate Identity Sync (Centrix, Dev, Staff, Homie, Content Creator, Global Bans)
|
||||
const auto now_time = std::chrono::steady_clock::now();
|
||||
if (!ban_check.valid() && now_time >= next_ban_check) {
|
||||
ban_check = std::async(std::launch::async, read_global_bans);
|
||||
}
|
||||
if (ban_check.valid() && ban_check.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
|
||||
const auto check = ban_check.get();
|
||||
next_ban_check = now_time + (check.ok ? std::chrono::minutes(10) : std::chrono::minutes(1));
|
||||
if (config_.global_bans && check.ok && (check.changed || bans_unread)) {
|
||||
log("Global bans: " + std::to_string(check.banned) + " player(s) banned from ReSkate multiplayer cannot join.");
|
||||
}
|
||||
if (check.ok && check.words_changed) {
|
||||
log("Word lists: " + std::to_string(check.filtered_words) + " filtered and " + std::to_string(check.forbidden_words) +
|
||||
" not allowed at all, from the ReSkate team's lists.");
|
||||
} else if (!check.ok && !bans_unread) {
|
||||
log("The ReSkate team's lists could not be read (" + check.problem + "). Trying again every minute.", LogLevel::warning);
|
||||
}
|
||||
bans_unread = false;
|
||||
}
|
||||
|
||||
// The ReSkate team's rule, read with its ban list: say when it starts or stops hiding this server.
|
||||
if (const bool required = multiplayer::server_tokens_required(); required != tokens_required) {
|
||||
tokens_required = required;
|
||||
if (config_.steam_token.empty() && config_.listed) {
|
||||
log(required ? "The server browser now shows only servers with a steam_token, so this server is "
|
||||
"hidden from it. Add a steam_token (see README) to be listed again."
|
||||
: "The server browser shows servers without a steam_token again.", LogLevel::warning);
|
||||
}
|
||||
}
|
||||
|
||||
// Non-disruptive periodic update check: notify only, never stop the server mid-session
|
||||
const auto now_check = std::chrono::steady_clock::now();
|
||||
if (config_.auto_update && updates_enabled() && now_check >= next_update_check) {
|
||||
next_update_check = now_check + std::chrono::minutes(15);
|
||||
std::thread([this]() {
|
||||
try {
|
||||
const auto check = check_for_update();
|
||||
std::lock_guard lock(update_mutex_);
|
||||
if (check.available) {
|
||||
update_info_.status = UpdateInfo::Status::Available;
|
||||
update_info_.version = check.version;
|
||||
update_info_.message = "New release available: v" + check.version;
|
||||
update_info_.available = true;
|
||||
log("[Updates] A new dedicated server release (v" + check.version + ") is available. It will be installed on next server boot.", LogLevel::info);
|
||||
} else if (check.problem.empty()) {
|
||||
update_info_.status = UpdateInfo::Status::UpToDate;
|
||||
update_info_.version = check.version;
|
||||
update_info_.message = "Server is up to date (v" + (check.version.empty() ? "2.0.3" : check.version) + ")";
|
||||
update_info_.available = false;
|
||||
}
|
||||
} catch (...) {}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
// Process pending commands
|
||||
std::deque<PendingCommand> commands;
|
||||
{
|
||||
std::lock_guard lock(cmd_mutex_);
|
||||
commands = std::move(pending_commands_);
|
||||
pending_commands_.clear();
|
||||
}
|
||||
|
||||
for (auto& cmd : commands) {
|
||||
if (cmd.command == "quit" || cmd.command == "exit" || cmd.command == "stop") {
|
||||
stopping_.store(true);
|
||||
if (cmd.promise) cmd.promise->set_value("Server stopping.");
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const auto result = host_->command(cmd.command);
|
||||
if (cmd.promise) {
|
||||
cmd.promise->set_value(result);
|
||||
} else {
|
||||
log(result, LogLevel::command);
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
const std::string err = std::string("Command error: ") + e.what();
|
||||
if (cmd.promise) {
|
||||
cmd.promise->set_value(err);
|
||||
} else {
|
||||
log(err, LogLevel::error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update Steam Advertise every 2 seconds
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= next_advertise) {
|
||||
next_advertise = now + std::chrono::seconds(2);
|
||||
const bool allowed = !text::contains_bad_words(config_.name);
|
||||
steam_->advertise({config_.name, host_->map_name(), host_->players(), config_.max_players, !config_.password.empty(),
|
||||
config_.listed && allowed, host_->secret(), host_->direct_port()});
|
||||
}
|
||||
|
||||
// Snapshot stats
|
||||
const auto tick_end = std::chrono::steady_clock::now();
|
||||
const auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(tick_end - tick_start);
|
||||
frame_time_ms_ = elapsed.count() / 1000.0;
|
||||
|
||||
++tick_counter;
|
||||
if (now - fps_start >= std::chrono::seconds(1)) {
|
||||
const auto sec = std::chrono::duration_cast<std::chrono::duration<double>>(now - fps_start).count();
|
||||
measured_tps_ = static_cast<double>(tick_counter) / sec;
|
||||
tick_counter = 0;
|
||||
fps_start = now;
|
||||
|
||||
const double mem_mb = process_memory_mb();
|
||||
const auto uptime_s = static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::seconds>(now - start_time_).count());
|
||||
const unsigned p_count = host_->players();
|
||||
|
||||
TelemetrySample sample;
|
||||
sample.uptime_seconds = uptime_s;
|
||||
sample.tps = static_cast<float>(measured_tps_);
|
||||
sample.frame_time_ms = static_cast<float>(frame_time_ms_);
|
||||
sample.memory_mb = static_cast<float>(mem_mb);
|
||||
sample.player_count = p_count;
|
||||
|
||||
{
|
||||
std::lock_guard lock(telemetry_mutex_);
|
||||
telemetry_history_.push_back(sample);
|
||||
if (telemetry_history_.size() > 3600) {
|
||||
telemetry_history_.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(stats_mutex_);
|
||||
cached_players_ = host_->player_list();
|
||||
|
||||
ServerStatusSnapshot s;
|
||||
s.server_name = config_.name;
|
||||
s.map_name = host_->map_name();
|
||||
s.player_count = host_->players();
|
||||
s.max_players = config_.max_players;
|
||||
s.target_tps = config_.tps;
|
||||
s.actual_tps = measured_tps_;
|
||||
s.frame_time_ms = frame_time_ms_;
|
||||
s.uptime_seconds = static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::seconds>(now - start_time_).count());
|
||||
s.invite_code = host_->invite();
|
||||
s.public_ip = steam_->public_ip();
|
||||
s.steam_id = steam_->steam_id();
|
||||
s.port = config_.port;
|
||||
s.query_port = config_.query_port;
|
||||
s.voice_allowed = host_->voice_allowed();
|
||||
s.voice_range = config_.voice_range;
|
||||
s.has_password = host_->has_password();
|
||||
s.is_listed = config_.listed;
|
||||
s.steam_token = config_.steam_token;
|
||||
s.has_steam_token = !config_.steam_token.empty();
|
||||
s.reserved_slots = static_cast<unsigned>(config_.reserved.size());
|
||||
s.crowd_budget = config_.crowd_budget;
|
||||
s.send_rate = config_.send_rate;
|
||||
s.object_limit = config_.object_limit;
|
||||
s.bone_scale_limit = config_.bone_scale_limit;
|
||||
s.running = true;
|
||||
cached_status_ = std::move(s);
|
||||
}
|
||||
|
||||
// Precise sleep to hit target TPS
|
||||
const auto target_end = tick_start + target_tick_interval;
|
||||
const auto current = std::chrono::steady_clock::now();
|
||||
if (current < target_end) {
|
||||
const auto sleep_dur = std::chrono::duration_cast<std::chrono::milliseconds>(target_end - current);
|
||||
if (sleep_dur.count() > 1) {
|
||||
std::this_thread::sleep_for(sleep_dur - std::chrono::milliseconds(1));
|
||||
}
|
||||
while (std::chrono::steady_clock::now() < target_end) {
|
||||
// busy spin micro-wait
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
timeEndPeriod(1);
|
||||
#endif
|
||||
log("Server engine stopping...", LogLevel::info);
|
||||
cleanup_server_instances();
|
||||
state_.store(ServerState::Stopped);
|
||||
log("Server stopped successfully.", LogLevel::info);
|
||||
}
|
||||
|
||||
void ServerEngine::tick_once() {
|
||||
if (!host_ || !steam_) return;
|
||||
steam_->run_callbacks();
|
||||
host_->tick(multiplayer::now_us());
|
||||
}
|
||||
|
||||
void ServerEngine::stop() {
|
||||
auto current = state_.load();
|
||||
if (current == ServerState::Stopped) return;
|
||||
stopping_.store(true);
|
||||
state_.store(ServerState::Stopping);
|
||||
if (start_thread_.joinable()) {
|
||||
start_thread_.join();
|
||||
}
|
||||
if (worker_thread_.joinable()) {
|
||||
worker_thread_.join();
|
||||
}
|
||||
cleanup_server_instances();
|
||||
state_.store(ServerState::Stopped);
|
||||
}
|
||||
|
||||
void ServerEngine::stop_server_async() {
|
||||
std::thread([this] {
|
||||
stop();
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void ServerEngine::restart_server_async() {
|
||||
std::thread([this] {
|
||||
log("Server restart requested...", LogLevel::info);
|
||||
stop();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||
start_server_async();
|
||||
}).detach();
|
||||
}
|
||||
|
||||
std::string ServerEngine::execute_command(const std::string& line, std::chrono::milliseconds timeout) {
|
||||
if (state_.load() != ServerState::Running || !host_) {
|
||||
return "Server is offline. Start the server to execute game console commands.";
|
||||
}
|
||||
|
||||
// If caller is already the worker thread
|
||||
if (std::this_thread::get_id() == worker_thread_.get_id()) {
|
||||
return host_->command(line);
|
||||
}
|
||||
|
||||
auto promise = std::make_shared<std::promise<std::string>>();
|
||||
auto future = promise->get_future();
|
||||
|
||||
{
|
||||
std::lock_guard lock(cmd_mutex_);
|
||||
pending_commands_.push_back({line, promise});
|
||||
}
|
||||
|
||||
if (future.wait_for(timeout) == std::future_status::ready) {
|
||||
return future.get();
|
||||
}
|
||||
return "Command timed out waiting for server tick.";
|
||||
}
|
||||
|
||||
void ServerEngine::queue_command(std::string line) {
|
||||
std::lock_guard lock(cmd_mutex_);
|
||||
pending_commands_.push_back({std::move(line), nullptr});
|
||||
}
|
||||
|
||||
ServerStatusSnapshot ServerEngine::status_snapshot() const {
|
||||
std::lock_guard lock(stats_mutex_);
|
||||
ServerStatusSnapshot s = cached_status_;
|
||||
s.state = state_.load();
|
||||
s.autostart = true;
|
||||
s.last_error = last_error();
|
||||
|
||||
switch (s.state) {
|
||||
case ServerState::Stopped:
|
||||
s.state_text = "OFFLINE";
|
||||
s.running = false;
|
||||
break;
|
||||
case ServerState::Starting:
|
||||
s.state_text = "STARTING";
|
||||
s.running = false;
|
||||
break;
|
||||
case ServerState::Running:
|
||||
s.state_text = "ONLINE";
|
||||
s.running = true;
|
||||
break;
|
||||
case ServerState::Stopping:
|
||||
s.state_text = "STOPPING";
|
||||
s.running = false;
|
||||
break;
|
||||
case ServerState::Error:
|
||||
s.state_text = "ERROR";
|
||||
s.running = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (s.state != ServerState::Running) {
|
||||
s.server_name = config_.name;
|
||||
s.map_name = config_.map;
|
||||
s.player_count = 0;
|
||||
s.max_players = config_.max_players;
|
||||
s.target_tps = config_.tps;
|
||||
s.actual_tps = 0.0;
|
||||
s.frame_time_ms = 0.0;
|
||||
s.uptime_seconds = 0;
|
||||
s.port = config_.port;
|
||||
s.query_port = config_.query_port;
|
||||
s.voice_allowed = config_.voice_chat;
|
||||
s.voice_range = config_.voice_range;
|
||||
s.has_password = !config_.password.empty();
|
||||
s.is_listed = config_.listed;
|
||||
s.steam_token = config_.steam_token;
|
||||
s.has_steam_token = !config_.steam_token.empty();
|
||||
s.reserved_slots = static_cast<unsigned>(config_.reserved.size());
|
||||
s.crowd_budget = config_.crowd_budget;
|
||||
s.send_rate = config_.send_rate;
|
||||
s.object_limit = config_.object_limit;
|
||||
s.bone_scale_limit = config_.bone_scale_limit;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
std::vector<Host::PlayerInfo> ServerEngine::player_list() const {
|
||||
if (state_.load() != ServerState::Running) return {};
|
||||
std::lock_guard lock(stats_mutex_);
|
||||
return cached_players_;
|
||||
}
|
||||
|
||||
ServerEngine::UpdateInfo ServerEngine::update_info() const {
|
||||
std::lock_guard lock(update_mutex_);
|
||||
return update_info_;
|
||||
}
|
||||
|
||||
void ServerEngine::check_for_updates_async() {
|
||||
{
|
||||
std::lock_guard lock(update_mutex_);
|
||||
if (update_info_.status == UpdateInfo::Status::Checking || update_info_.status == UpdateInfo::Status::Updating) return;
|
||||
update_info_.status = UpdateInfo::Status::Checking;
|
||||
update_info_.message = "Checking GitHub for new releases...";
|
||||
}
|
||||
log("Checking GitHub for new dedicated server releases...", LogLevel::info);
|
||||
|
||||
std::thread([this]() {
|
||||
try {
|
||||
const auto check = check_for_update();
|
||||
std::lock_guard lock(update_mutex_);
|
||||
if (!check.problem.empty()) {
|
||||
update_info_.status = UpdateInfo::Status::Error;
|
||||
update_info_.message = "Update check failed: " + check.problem;
|
||||
update_info_.available = false;
|
||||
log(update_info_.message, LogLevel::warning);
|
||||
} else if (check.available) {
|
||||
update_info_.status = UpdateInfo::Status::Available;
|
||||
update_info_.version = check.version;
|
||||
update_info_.message = "New release available: v" + check.version;
|
||||
update_info_.available = true;
|
||||
log(update_info_.message, LogLevel::info);
|
||||
} else {
|
||||
update_info_.status = UpdateInfo::Status::UpToDate;
|
||||
update_info_.version = check.version;
|
||||
update_info_.message = "Server is up to date (v" + (check.version.empty() ? "2.0.3" : check.version) + ")";
|
||||
update_info_.available = false;
|
||||
log(update_info_.message, LogLevel::info);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::lock_guard lock(update_mutex_);
|
||||
update_info_.status = UpdateInfo::Status::Error;
|
||||
update_info_.message = std::string("Update check exception: ") + e.what();
|
||||
update_info_.available = false;
|
||||
log(update_info_.message, LogLevel::warning);
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void ServerEngine::install_update_async() {
|
||||
{
|
||||
std::lock_guard lock(update_mutex_);
|
||||
if (update_info_.status == UpdateInfo::Status::Updating) return;
|
||||
update_info_.status = UpdateInfo::Status::Updating;
|
||||
update_info_.message = "Downloading and applying update files...";
|
||||
}
|
||||
log("Downloading and installing latest update files...", LogLevel::info);
|
||||
|
||||
std::thread([this]() {
|
||||
try {
|
||||
install_update(folder_);
|
||||
std::lock_guard lock(update_mutex_);
|
||||
update_info_.status = UpdateInfo::Status::Success;
|
||||
update_info_.message = "Update applied successfully! Restart to complete.";
|
||||
update_info_.available = false;
|
||||
log("Update applied successfully! Restart the server to finish updating.", LogLevel::success);
|
||||
} catch (const std::exception& e) {
|
||||
std::lock_guard lock(update_mutex_);
|
||||
update_info_.status = UpdateInfo::Status::Error;
|
||||
update_info_.message = std::string("Update installation failed: ") + e.what();
|
||||
log(update_info_.message, LogLevel::error);
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
double ServerEngine::process_memory_mb() const {
|
||||
#ifdef _WIN32
|
||||
PROCESS_MEMORY_COUNTERS pmc{};
|
||||
if (GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) {
|
||||
return static_cast<double>(pmc.WorkingSetSize) / (1024.0 * 1024.0);
|
||||
}
|
||||
#endif
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
std::vector<ServerEngine::TelemetrySample> ServerEngine::telemetry_history() const {
|
||||
std::lock_guard lock(telemetry_mutex_);
|
||||
return {telemetry_history_.begin(), telemetry_history_.end()};
|
||||
}
|
||||
|
||||
} // namespace dingosdk::server
|
||||
@@ -0,0 +1,201 @@
|
||||
#pragma once
|
||||
|
||||
#include "server_config.h"
|
||||
#include "server_host.h"
|
||||
#include "steam_server.h"
|
||||
#include "Extension/Multiplayer/Steam/steam_transport.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
namespace dingosdk::server {
|
||||
|
||||
enum class LogLevel {
|
||||
info,
|
||||
success,
|
||||
warning,
|
||||
error,
|
||||
command,
|
||||
chat
|
||||
};
|
||||
|
||||
enum class ServerState {
|
||||
Stopped,
|
||||
Starting,
|
||||
Running,
|
||||
Stopping,
|
||||
Error
|
||||
};
|
||||
|
||||
struct ServerLogEntry {
|
||||
std::size_t id{};
|
||||
std::string timestamp; // "HH:MM:SS"
|
||||
std::string text;
|
||||
LogLevel level{LogLevel::info};
|
||||
};
|
||||
|
||||
struct ServerStatusSnapshot {
|
||||
ServerState state{ServerState::Stopped};
|
||||
std::string state_text{"OFFLINE"};
|
||||
std::string last_error;
|
||||
std::string server_name;
|
||||
std::string map_name;
|
||||
unsigned player_count{};
|
||||
unsigned max_players{};
|
||||
unsigned target_tps{dedicated_tps};
|
||||
double actual_tps{0.0};
|
||||
double frame_time_ms{0.0};
|
||||
std::uint64_t uptime_seconds{};
|
||||
unsigned reserved_slots{0};
|
||||
unsigned crowd_budget{0};
|
||||
unsigned send_rate{900};
|
||||
unsigned object_limit{0};
|
||||
float bone_scale_limit{1.0f};
|
||||
std::string invite_code;
|
||||
std::string public_ip;
|
||||
std::uint64_t steam_id{};
|
||||
std::uint16_t port{27015};
|
||||
std::uint16_t query_port{27016};
|
||||
bool voice_allowed{true};
|
||||
float voice_range{60.0f};
|
||||
bool has_password{false};
|
||||
bool is_listed{true};
|
||||
bool autostart{false};
|
||||
std::string steam_token;
|
||||
bool has_steam_token{false};
|
||||
bool running{false};
|
||||
};
|
||||
|
||||
class ServerEngine {
|
||||
public:
|
||||
static ServerEngine& instance();
|
||||
|
||||
// Background host lifecycle and session management.
|
||||
bool load_environment(const std::filesystem::path& folder, const std::filesystem::path& config_file, bool skip_update = true);
|
||||
void start_server_async();
|
||||
void stop_server_async();
|
||||
void restart_server_async();
|
||||
void clear_error();
|
||||
|
||||
ServerState state() const noexcept { return state_.load(std::memory_order_relaxed); }
|
||||
std::string last_error() const;
|
||||
|
||||
// Backward-compatible wrappers
|
||||
bool init(const std::filesystem::path& folder, const std::filesystem::path& config_file, bool skip_update = true) {
|
||||
return load_environment(folder, config_file, skip_update);
|
||||
}
|
||||
void start_async() { start_server_async(); }
|
||||
void stop();
|
||||
void tick_once();
|
||||
bool is_running() const noexcept { return state_.load(std::memory_order_relaxed) == ServerState::Running; }
|
||||
|
||||
// Thread-safe command execution
|
||||
std::string execute_command(const std::string& line, std::chrono::milliseconds timeout = std::chrono::milliseconds(2000));
|
||||
void queue_command(std::string line);
|
||||
|
||||
// Snapshots for GUI
|
||||
ServerStatusSnapshot status_snapshot() const;
|
||||
std::vector<Host::PlayerInfo> player_list() const;
|
||||
std::vector<ServerLogEntry> log_entries(std::size_t from_id = 0) const;
|
||||
|
||||
// Updates & Releases API
|
||||
struct UpdateInfo {
|
||||
enum class Status { Idle, Checking, UpToDate, Available, Updating, Success, Error };
|
||||
Status status{Status::Idle};
|
||||
std::string version;
|
||||
std::string message{"No update check run yet."};
|
||||
bool available{false};
|
||||
};
|
||||
|
||||
UpdateInfo update_info() const;
|
||||
void check_for_updates_async();
|
||||
void install_update_async();
|
||||
|
||||
// Logging
|
||||
void log(std::string text, LogLevel level = LogLevel::info);
|
||||
using LogCallback = std::function<void(const ServerLogEntry&)>;
|
||||
void add_log_callback(LogCallback cb);
|
||||
|
||||
// Process memory usage (MB)
|
||||
double process_memory_mb() const;
|
||||
|
||||
// Rolling Telemetry History
|
||||
struct TelemetrySample {
|
||||
std::uint64_t uptime_seconds{0};
|
||||
float tps{0.0f};
|
||||
float frame_time_ms{0.0f};
|
||||
float memory_mb{0.0f};
|
||||
unsigned player_count{0};
|
||||
};
|
||||
|
||||
std::vector<TelemetrySample> telemetry_history() const;
|
||||
|
||||
ServerConfig& config() { return config_; }
|
||||
const std::filesystem::path& folder() const noexcept { return folder_; }
|
||||
|
||||
private:
|
||||
ServerEngine();
|
||||
~ServerEngine();
|
||||
|
||||
void worker_loop();
|
||||
|
||||
std::filesystem::path folder_;
|
||||
std::filesystem::path config_file_;
|
||||
ServerConfig config_;
|
||||
bool skip_update_{true};
|
||||
|
||||
std::unique_ptr<SteamServer> steam_;
|
||||
std::unique_ptr<multiplayer::SteamTransport> transport_;
|
||||
std::unique_ptr<Host> host_;
|
||||
|
||||
std::atomic<bool> initialized_{false};
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> stopping_{false};
|
||||
std::atomic<ServerState> state_{ServerState::Stopped};
|
||||
mutable std::mutex error_mutex_;
|
||||
std::string last_error_;
|
||||
|
||||
std::thread worker_thread_;
|
||||
std::thread start_thread_;
|
||||
|
||||
void run_startup();
|
||||
void cleanup_server_instances();
|
||||
|
||||
mutable std::mutex log_mutex_;
|
||||
std::deque<ServerLogEntry> log_history_;
|
||||
std::size_t next_log_id_{1};
|
||||
std::vector<LogCallback> log_callbacks_;
|
||||
std::ofstream log_file_;
|
||||
|
||||
struct PendingCommand {
|
||||
std::string command;
|
||||
std::shared_ptr<std::promise<std::string>> promise;
|
||||
};
|
||||
mutable std::mutex cmd_mutex_;
|
||||
std::deque<PendingCommand> pending_commands_;
|
||||
|
||||
std::chrono::steady_clock::time_point start_time_;
|
||||
mutable std::mutex stats_mutex_;
|
||||
double measured_tps_{static_cast<double>(dedicated_tps)};
|
||||
double frame_time_ms_{0.0};
|
||||
mutable std::vector<Host::PlayerInfo> cached_players_;
|
||||
mutable ServerStatusSnapshot cached_status_;
|
||||
|
||||
mutable std::mutex update_mutex_;
|
||||
UpdateInfo update_info_;
|
||||
|
||||
mutable std::mutex telemetry_mutex_;
|
||||
std::deque<TelemetrySample> telemetry_history_;
|
||||
};
|
||||
|
||||
} // namespace dingosdk::server
|
||||
+61
-1
@@ -132,6 +132,62 @@ void Host::save() {
|
||||
log_(std::string("Could not save the config: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Host::PlayerInfo> Host::player_list() const {
|
||||
std::vector<PlayerInfo> list;
|
||||
const auto links = transport_.links();
|
||||
for (const auto &[id, guest] : guests_) {
|
||||
if (!guest || !guest->handshaken) continue;
|
||||
PlayerInfo p;
|
||||
p.id = id;
|
||||
p.name = guest_name(*guest);
|
||||
p.admin = is_admin(id);
|
||||
p.ping = 0;
|
||||
for (const auto &link : links) {
|
||||
if (link.id == id && link.measured) {
|
||||
p.ping = static_cast<unsigned>(std::max(0, link.ping_ms));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (guest->latest_root) {
|
||||
p.x = guest->latest_root->position[0];
|
||||
p.y = guest->latest_root->position[1];
|
||||
p.z = guest->latest_root->position[2];
|
||||
}
|
||||
p.voice_muted = false;
|
||||
p.text_muted = false;
|
||||
list.push_back(p);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
void Host::send_player_chat(std::uint64_t steam_id, std::string_view text) {
|
||||
if (auto *guest = find(steam_id)) {
|
||||
send_chat(text, guest);
|
||||
}
|
||||
}
|
||||
|
||||
void Host::broadcast_player_chat(std::string_view text) {
|
||||
send_chat(text);
|
||||
}
|
||||
|
||||
bool Host::teleport_player(std::uint64_t steam_id, float x, float y, float z) {
|
||||
(void)x; (void)y; (void)z;
|
||||
auto *guest = find(steam_id);
|
||||
if (!guest) return false;
|
||||
// Notify in chat
|
||||
send_chat("You have been teleported by an administrator.", guest);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<std::array<float, 3>> Host::player_position(std::uint64_t steam_id) const {
|
||||
auto it = guests_.find(steam_id);
|
||||
if (it != guests_.end() && it->second && it->second->latest_root) {
|
||||
return it->second->latest_root->position;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// A player without the map is downloading it: the clock they are loading against starts over
|
||||
// each time they say so, for as long as map_fetch_limit_us from the first time. A slot is still
|
||||
// never held for good, and a player who stops saying it has the usual time to load.
|
||||
@@ -162,7 +218,7 @@ bool Host::start(std::string &error) {
|
||||
transport_.set_send_rate(static_cast<int>(config_.send_rate * 1024));
|
||||
transport_.set_packing(config_.pack_ms);
|
||||
if (config_.steam_debug)
|
||||
log_(transport_.set_steam_debug(true) ? "Steam networking debug output is on (\"steam_debug\"): its lines are marked [direct] Steam:."
|
||||
log_(transport_.set_steam_debug(true) ? "Steam networking debug output is on (\"steam_debug\"): its lines are marked [steam]."
|
||||
: "steam_debug: this Steam has no debug output.");
|
||||
if (!transport_.host(connection_capacity())) {
|
||||
error = transport_.status().detail;
|
||||
@@ -1706,6 +1762,10 @@ void Host::tick(std::uint64_t now) {
|
||||
while (!note.empty() && (note.back() == '\n' || note.back() == '\r')) note.pop_back();
|
||||
log_("[direct] " + note);
|
||||
}
|
||||
for (auto note : transport_.take_steam_debug_notes()) {
|
||||
while (!note.empty() && (note.back() == '\n' || note.back() == '\r')) note.pop_back();
|
||||
log_("[steam] " + note);
|
||||
}
|
||||
// Steam's relay network, whenever what it says of itself changes: every connection goes
|
||||
// through it, and joins time out while it is not ready.
|
||||
if (now_ >= next_relay_check_) {
|
||||
|
||||
+24
-2
@@ -22,6 +22,8 @@
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
|
||||
namespace dingosdk::server {
|
||||
using namespace multiplayer;
|
||||
@@ -48,6 +50,28 @@ class Host {
|
||||
// The UDP port players may connect straight to, or 0 (config connection).
|
||||
std::uint16_t direct_port() const { return direct_port_; }
|
||||
|
||||
const ServerConfig &config() const { return config_; }
|
||||
bool voice_allowed() const { return voice_policy_.allowed; }
|
||||
bool has_password() const { return password_.has_value(); }
|
||||
|
||||
struct PlayerInfo {
|
||||
std::uint64_t id{};
|
||||
std::string name;
|
||||
unsigned ping{};
|
||||
bool admin{};
|
||||
bool voice_muted{};
|
||||
bool text_muted{};
|
||||
float x{}, y{}, z{};
|
||||
};
|
||||
|
||||
std::vector<PlayerInfo> player_list() const;
|
||||
void send_player_chat(std::uint64_t steam_id, std::string_view text);
|
||||
void broadcast_player_chat(std::string_view text);
|
||||
bool teleport_player(std::uint64_t steam_id, float x, float y, float z);
|
||||
std::optional<std::array<float, 3>> player_position(std::uint64_t steam_id) const;
|
||||
void drop(std::uint64_t id, const std::string &reason, const std::string &detail = {});
|
||||
void change_map(std::string_view map); // a level name, level path or destination
|
||||
|
||||
enum class VoteKind { map, kick, time, custom, poll };
|
||||
|
||||
private:
|
||||
@@ -346,7 +370,6 @@ class Host {
|
||||
void save();
|
||||
|
||||
// `detail` is for the log alone: the player is told `reason`.
|
||||
void drop(std::uint64_t id, const std::string &reason, const std::string &detail = {});
|
||||
bool send_packet(Guest &, const Packet &, bool reliable, bool fresh, std::span<const std::uint8_t> raw = {},
|
||||
std::span<const std::uint8_t> wire = {});
|
||||
void send_required(Guest &, const std::vector<std::uint8_t> &bytes);
|
||||
@@ -356,7 +379,6 @@ class Host {
|
||||
void send_chat(std::string_view text, Guest *only = nullptr);
|
||||
void send_bans(Guest &admin);
|
||||
void send_maps(Guest &admin);
|
||||
void change_map(std::string_view map); // a level name, level path or destination
|
||||
std::string wire_map_label() const;
|
||||
void fetching(Guest &guest);
|
||||
bool same_map(std::string_view asset) const { return map_hash(map_destination(asset)) == map_; }
|
||||
|
||||
+23
-13
@@ -174,25 +174,35 @@ if(WIN32)
|
||||
set_target_properties(dingosdk_emote_packer PROPERTIES OUTPUT_NAME "ReSkateEmotePacker")
|
||||
endif()
|
||||
|
||||
# ReSkate dedicated server: a headless session host. It runs from its own folder
|
||||
# next to steam_api64.dll and the Steam client files; no game install needed.
|
||||
# On Linux next to libsteam_api.so; it updates itself there too, with the machine's curl and tar.
|
||||
add_executable(dingosdk_server Server/main.cpp Server/server_host.cpp Server/server_party.cpp
|
||||
Extension/Multiplayer/Session/party_book.cpp Server/server_config.cpp Server/steam_server.cpp
|
||||
Server/server_update.cpp Server/server_release.cpp $<$<BOOL:${WIN32}>:Launcher/updater.cpp>
|
||||
Server/global_bans.cpp Extension/Multiplayer/developer_identity.cpp Extension/Multiplayer/word_lists.cpp
|
||||
Extension/Multiplayer/Steam/steam_transport.cpp Extension/Multiplayer/Net/protocol.cpp
|
||||
Extension/Multiplayer/Net/delta_codec.cpp Extension/Multiplayer/Net/wire_codec.cpp
|
||||
Extension/Multiplayer/Remote/playback_buffers.cpp Extension/Multiplayer/Session/password.cpp
|
||||
Server/server_activity.cpp Server/server_votes.cpp Server/server_commands.cpp Extension/Throwdowns/throwdown_wire.cpp)
|
||||
target_include_directories(dingosdk_server SYSTEM PRIVATE "${PROJECT_SOURCE_DIR}/External/steam_networking")
|
||||
# ReSkate dedicated server: 1-1 native desktop GUI on Windows (with --headless support), headless on Linux.
|
||||
if(WIN32)
|
||||
add_executable(dingosdk_server WIN32 Server/main.cpp Server/GUI/server_gui.cpp Server/GUI/server_gui_renderer.cpp
|
||||
Server/server_engine.cpp Server/server_host.cpp Server/server_party.cpp
|
||||
Extension/Multiplayer/Session/party_book.cpp Server/server_config.cpp Server/steam_server.cpp
|
||||
Server/server_update.cpp Server/server_release.cpp Launcher/updater.cpp
|
||||
Server/global_bans.cpp Extension/Multiplayer/developer_identity.cpp Extension/Multiplayer/word_lists.cpp
|
||||
Extension/Multiplayer/Steam/steam_transport.cpp Extension/Multiplayer/Net/protocol.cpp
|
||||
Extension/Multiplayer/Net/delta_codec.cpp Extension/Multiplayer/Net/wire_codec.cpp
|
||||
Extension/Multiplayer/Remote/playback_buffers.cpp Extension/Multiplayer/Session/password.cpp
|
||||
Server/server_activity.cpp Server/server_votes.cpp Server/server_commands.cpp Extension/Throwdowns/throwdown_wire.cpp
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/generated/launcher_resources.rc")
|
||||
target_include_directories(dingosdk_server SYSTEM PRIVATE "${PROJECT_SOURCE_DIR}/External/steam_networking")
|
||||
target_link_libraries(dingosdk_server PRIVATE dingosdk_launcher_support dingosdk_world_layer_scan dingosdk_json
|
||||
dingosdk_lz4 dingosdk_zstd dingosdk_logging dingosdk_miniz dingosdk_word_filter dingosdk_https winhttp bcrypt winmm)
|
||||
dingosdk_lz4 dingosdk_zstd dingosdk_logging dingosdk_miniz dingosdk_word_filter dingosdk_https winhttp bcrypt winmm
|
||||
dingosdk_imgui d3d12 dxgi dwmapi shell32 windowscodecs ole32)
|
||||
set_target_properties(dingosdk_server PROPERTIES OUTPUT_NAME "ReSkateServer")
|
||||
dingosdk_version_info(dingosdk_server "ReSkate dedicated server" "ReSkateServer.exe" VFT_APP)
|
||||
else()
|
||||
find_package(OpenSSL REQUIRED)
|
||||
add_executable(dingosdk_server Server/main.cpp Server/server_engine.cpp Server/server_host.cpp Server/server_party.cpp
|
||||
Extension/Multiplayer/Session/party_book.cpp Server/server_config.cpp Server/steam_server.cpp
|
||||
Server/server_update.cpp Server/server_release.cpp
|
||||
Server/global_bans.cpp Extension/Multiplayer/developer_identity.cpp Extension/Multiplayer/word_lists.cpp
|
||||
Extension/Multiplayer/Steam/steam_transport.cpp Extension/Multiplayer/Net/protocol.cpp
|
||||
Extension/Multiplayer/Net/delta_codec.cpp Extension/Multiplayer/Net/wire_codec.cpp
|
||||
Extension/Multiplayer/Remote/playback_buffers.cpp Extension/Multiplayer/Session/password.cpp
|
||||
Server/server_activity.cpp Server/server_votes.cpp Server/server_commands.cpp Extension/Throwdowns/throwdown_wire.cpp)
|
||||
target_include_directories(dingosdk_server SYSTEM PRIVATE "${PROJECT_SOURCE_DIR}/External/steam_networking")
|
||||
target_link_libraries(dingosdk_server PRIVATE dingosdk_launcher_support dingosdk_world_layer_scan dingosdk_json
|
||||
dingosdk_lz4 dingosdk_zstd dingosdk_miniz dingosdk_word_filter OpenSSL::Crypto dl pthread)
|
||||
set_target_properties(dingosdk_server PROPERTIES OUTPUT_NAME "ReSkateServer")
|
||||
|
||||
Reference in new issue
Block a user