mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-26 23:16:41 +01:00
config in sections
Server config: - Written in sections, in reading order: server, access, maps, players, anti_cheat, network, votes. A config from an older version, flat and under older names, is read as it is and written back this way on first start; only settings that are really new are reported as new. - Renamed: no_bail, noclip, boosts, parties, voice_chat to allow_*; global_bans to use_global_bans; welcome to welcome_message; speed_check to speed_hack; score_check to modified_scoring; score_allow to allowed_scoring_mods; map_pool and map_rotation_minutes to maps.pool and maps.rotation_minutes. Console and admin commands are unchanged. - Bans are kept in data/bans.json beside the config. A config that holds them has them moved there; a bans file that cannot be read stops the server from starting. - Reserved slots are one list, "reserved_players_slots": a slot is kept for each listed player who is not on. "reserved_slots" and the "reserved slots <n>" command are gone; admins may take a kept slot but none is kept for them. - "tps" is no longer in the file: dedicated servers run at 20. - "bone_scale_limit" is 1 by default: resized body parts are off unless a server allows them. - New: "pack_ms" (10), how long Steam may hold a message to pack it with others; "finger_distance" (25); "steam_debug", which logs what Steam's networking says, for connection faults.
This commit is contained in:
1 parent
38d86e1b72
commit
2833ab1f88
9 files changed
+531
-384
No files matched your search
+140
-128
@@ -42,93 +42,18 @@ straight away (players are told to rejoin). Turn this off with
|
||||
|
||||
ReSkateServer.json
|
||||
------------------
|
||||
The settings are in sections; each setting below is listed under the section it is in,
|
||||
like "server": {"name": "My server"}. A file from an older version, with every setting at
|
||||
the top and some under older names, is read as it is and written back this way the first
|
||||
time the server starts.
|
||||
|
||||
"server" - The server itself: its name, who it lets in by password, and its ports.
|
||||
name Shown in the browser: 1-64 letters, numbers, spaces and - _ / [ ] ( ).
|
||||
map The map everyone skates, named like the game's load command:
|
||||
"San Vansterdam", "Isle of Grom", "Super Ultra Mega Resort",
|
||||
"Stadium 1", or a custom map such as "bbcity" (see Custom maps).
|
||||
map_pool The maps players may vote for and the rotation goes through,
|
||||
in order, e.g. ["San Vansterdam", "Isle of Grom", "bbcity"].
|
||||
Empty (the default) allows every map the server knows.
|
||||
Admins can still change to any map.
|
||||
map_rotation_minutes Minutes on each map before the server moves to the next
|
||||
one in map_pool (default 0: off). Players get a minute's
|
||||
warning; the clock waits while nobody is on, and starts
|
||||
over whenever the map changes (by a vote or an admin too).
|
||||
max_players 1-249.
|
||||
send_rate The most the server sends one player, in KB/s (default 900,
|
||||
128-16384). About 1100 is what reaches a player through
|
||||
Steam's relays: set higher, what is lost is resent until the
|
||||
connection is full of resends and half of everything is
|
||||
lost. Console: rate <KB/s>, also for the players already on.
|
||||
finger_distance Past this many metres (default 25) a player's fingers are not
|
||||
sent moving: they stay as they were, and move again when the
|
||||
player is nearer. Fingers are nearly half of every position
|
||||
update and cannot be made out at that distance. 0 always
|
||||
sends them.
|
||||
pack_ms How long a message to a player may wait to go in the same
|
||||
packet as the next ones, in milliseconds (default 10, 0 to
|
||||
50). A full server sends each player hundreds of small
|
||||
messages a second, each in a packet of its own with its own
|
||||
headers; packed, the same updates take fewer packets, less
|
||||
bandwidth and less CPU. It adds up to that long to when an
|
||||
update arrives. Voice is never held back. 0 sends every
|
||||
message at once, as before. Takes effect on restart.
|
||||
connection How players reach the server: "relay" (default) or "direct".
|
||||
relay Through Steam's relay network. Nothing to open, and
|
||||
the server never sees a player's address. The route
|
||||
is Steam's choice and can be a long way round.
|
||||
direct Straight to this server's "port" over UDP, which
|
||||
must be open to the internet. The shortest route,
|
||||
so the lowest ping. The server sees the addresses
|
||||
of the players who connect this way, as any
|
||||
dedicated server does; players never see each
|
||||
other's.
|
||||
A direct server still answers through the relays: a game
|
||||
tries the port first and uses the relays if it gets no
|
||||
answer in a few seconds, and a player can turn direct
|
||||
connections off in their own settings. So "direct" never
|
||||
keeps anyone out. Steam vouches for who each player is
|
||||
either way. "net" shows who is direct, and the log has a
|
||||
[direct] line for each player who asks to connect that way.
|
||||
direct_address For "direct": the address players connect to, like
|
||||
203.0.113.7, when it is not the one the server's start-up
|
||||
line calls its public IP. Some hosts send from one address
|
||||
and take connections on another; use the one your panel
|
||||
shows for the server. Empty (default): the public IP.
|
||||
steam_debug true logs what Steam's own networking says it is doing: each
|
||||
connection asked for, and what it refused or ignored and
|
||||
why. For finding out why players cannot connect; it is a
|
||||
lot of text, so turn it off again afterwards.
|
||||
relay_everything true sends poses through the server even between players
|
||||
whose games have linked to each other. For testing with a
|
||||
few players; leave it false otherwise.
|
||||
direct_port For "direct": the UDP port to listen on when it is not
|
||||
"port" (default 0: "port"). Must differ from query_port.
|
||||
bone_scale_limit How far a mod may resize part of a skater for the other
|
||||
players (a "big head" mod and the like): the most a bone may
|
||||
be scaled, 1 to 8. 1 (default) shows every skater at the
|
||||
game's own proportions; 0 is no limit. The player with the
|
||||
mod still sees it on their own screen. Console:
|
||||
bone-scale <1-8>|off.
|
||||
crowd_budget The most position updates a second one player is sent
|
||||
(default 600, 0 for no limit). Players near each other are
|
||||
sent at the full rate (tps); this only matters once more are
|
||||
in one place than budget / tps, about 30 at 20 TPS. Then the
|
||||
nearest stay at the full rate and the farthest of the crowd
|
||||
drop to 10 and 5 a second, instead of everyone's connection
|
||||
filling up. About 1000 is what reaches a player through
|
||||
Steam's relays; more than that was seen to lose half.
|
||||
Console: crowd <n>|off.
|
||||
reserved_slots How many of those are kept for the players in "reserved" and
|
||||
the admins (default 0). With 64 players and 4 reserved slots,
|
||||
anyone can join until 60 are on; the last 4 only they can
|
||||
take. The browser still shows 64, and everyone else is told
|
||||
the remaining slots are reserved.
|
||||
reserved SteamID64s of the players with a reserved slot, e.g.
|
||||
["76561198000000000"]. Admins always have one.
|
||||
password Empty for anyone; otherwise players type it to join.
|
||||
welcome A chat line sent to each player as they join.
|
||||
welcome_message A chat line sent to each player as they join.
|
||||
listed false hides the server; players then need the code.
|
||||
max_players 1-249.
|
||||
port, query_port Steam game server ports (default 27015, 27016).
|
||||
steam_token A Steam game server login token, or empty (default). Without one
|
||||
the server signs in anonymously and gets a new Steam ID every
|
||||
start. With one it keeps the same Steam ID, printed at startup.
|
||||
@@ -139,36 +64,70 @@ steam_token A Steam game server login token, or empty (default). Without
|
||||
only servers that have one; the server says so in its log when
|
||||
that hides it. Players can always join with the code.
|
||||
auto_update Install new ReSkate releases when nobody is on (default true).
|
||||
global_bans Turn away players the ReSkate team has banned from multiplayer
|
||||
activity_log Log what players do (default true): throwdown drops placed,
|
||||
joins, starts, turns and results; objects placed or removed;
|
||||
how long players take to load.
|
||||
|
||||
"access" - Who runs the server and who always has a place on it.
|
||||
admins SteamID64s (as strings) who may change settings in-game.
|
||||
reserved_players_slots
|
||||
SteamID64s of the players with a reserved slot, e.g.
|
||||
["76561198000000000"] (default none). A slot is kept free
|
||||
for each of them who is not on: with 64 players and 4
|
||||
listed, none of them on, anyone can join until 60 are on
|
||||
and only they can take the last 4. One who is on has taken
|
||||
theirs. The browser still shows 64, and everyone else is
|
||||
told the remaining slots are reserved. Admins may take a
|
||||
kept slot too, but none is kept for them. Must list fewer
|
||||
players than max_players.
|
||||
use_global_bans Turn away players the ReSkate team has banned from multiplayer
|
||||
(default true). The list is read from api.reskate.dev at startup
|
||||
and every ten minutes. false lets them in; the server's own
|
||||
"bans" apply either way.
|
||||
votes Player votes, each off until turned on:
|
||||
"map": {"enabled": true, "percent": 60} /vote map <map>
|
||||
(a map in map_pool)
|
||||
"kick": {"enabled": true, "percent": 60} /vote kick <player>
|
||||
"time_of_day": {"enabled": true, "percent": 50} /vote tod <time>
|
||||
(needs world_layer_sync)
|
||||
"percent" is the share of connected players whose yes passes
|
||||
it. "seconds" (default 30) is how long a vote runs and
|
||||
"cooldown_seconds" (default 60) how long a player waits before
|
||||
starting another. Players vote with /yes and /no in chat;
|
||||
admins cannot be vote-kicked.
|
||||
announce_throwdowns Tell everyone in chat when a throwdown drop is placed
|
||||
(default true).
|
||||
parties Let players form parties (default true): invite each other
|
||||
bans (data/bans.json) apply either way.
|
||||
|
||||
"maps" - What is skated.
|
||||
map The map everyone skates, named like the game's load command:
|
||||
"San Vansterdam", "Isle of Grom", "Super Ultra Mega Resort",
|
||||
"Stadium 1", or a custom map such as "bbcity" (see Custom maps).
|
||||
pool The maps players may vote for and the rotation goes through,
|
||||
in order, e.g. ["San Vansterdam", "Isle of Grom", "bbcity"].
|
||||
Empty (the default) allows every map the server knows.
|
||||
Admins can still change to any map.
|
||||
rotation_minutes Minutes on each map before the server moves to the next
|
||||
one in pool (default 0: off). Players get a minute's
|
||||
warning; the clock waits while nobody is on, and starts
|
||||
over whenever the map changes (by a vote or an admin too).
|
||||
parks Layout for each park lot, e.g. "skatepark_01", or "empty".
|
||||
world_layer_sync Force the "layers" below on every player.
|
||||
layers World layer key -> "on" / "off".
|
||||
|
||||
"players" - What players may do.
|
||||
allow_noclip, allow_no_bail, allow_boosts
|
||||
Let players use noclip (and tp) / No Bail / the forward and up
|
||||
boosts (default true; admins always can).
|
||||
allow_parties Let players form parties (default true): invite each other
|
||||
from the game's Social menu, a player card, the ReSkate
|
||||
Multiplayer menu or chat (/party invite <player>). Party
|
||||
members join each other's coop challenges, see each other on
|
||||
the map and talk with /p <message>.
|
||||
party_size Most players in one party, 2-8 (default 8).
|
||||
speed_check Catch players whose game runs faster than normal (Cheat
|
||||
allow_voice_chat Allow voice chat.
|
||||
voice_range How far proximity voice reaches, 50-1000 m.
|
||||
object_placement everyone, admins (only admins can build), or nobody.
|
||||
object_limit How many objects each player may have placed, 1-1024
|
||||
(default 100), or 0 for no limit. Admins are not limited.
|
||||
A player at the limit deletes one to place another.
|
||||
announce_throwdowns Tell everyone in chat when a throwdown drop is placed
|
||||
(default true).
|
||||
|
||||
"anti_cheat" - What the server checks, and what it does about it.
|
||||
speed_hack Catch players whose game runs faster than normal (Cheat
|
||||
Engine's speedhack and the like), measured from the timing
|
||||
of what their game sends: "warn" (default) takes them out of
|
||||
throwdowns and coop challenges until their speed is normal
|
||||
again and tells them so (the log has it for the admins), "kick" removes them from the
|
||||
server, "off" does not check.
|
||||
score_check Catch players whose mods change how many points tricks
|
||||
modified_scoring Catch players whose mods change how many points tricks
|
||||
score (per-trick points, the scoring multipliers, the
|
||||
throwdown scoring logic) or how the skater handles (core
|
||||
physics, wipeouts, trick gestures). Each player's ReSkate checks its
|
||||
@@ -178,37 +137,91 @@ score_check Catch players whose mods change how many points tricks
|
||||
messages on) and everyone is told in chat, "kick" removes
|
||||
them, "off" does not check. A player has to restart Skate
|
||||
without the mod to take part again.
|
||||
score_allow Scoring fingerprints accepted like the game's own, for a
|
||||
allowed_scoring_mods Scoring fingerprints accepted like the game's own, for a
|
||||
server that runs on a scoring mod everyone installs
|
||||
(16 hex digits each; score-check lists each player's).
|
||||
activity_log Log what players do (default true): throwdown drops placed,
|
||||
joins, starts, turns and results; objects placed or removed;
|
||||
how long players take to load.
|
||||
port, query_port Steam game server ports (default 27015, 27016).
|
||||
tps Network updates per second. Fixed at 20 on dedicated servers
|
||||
for now: whatever is set here is read as 20.
|
||||
voice_chat Allow voice chat.
|
||||
voice_range How far proximity voice reaches, 50-1000 m.
|
||||
distances When far-away players update less often (metres).
|
||||
object_placement everyone, admins (only admins can build), or nobody.
|
||||
object_limit How many objects each player may have placed, 1-1024
|
||||
(default 100), or 0 for no limit. Admins are not limited.
|
||||
A player at the limit deletes one to place another.
|
||||
noclip, no_bail, Let players use noclip (and tp) / No Bail / the forward and up
|
||||
boosts boosts (default true; admins always can).
|
||||
enforce_tuning Players skate with the game's own Gameplay/SkatePhysicsTuning,
|
||||
not copies they edited (default true). Edited tuning (truck
|
||||
positions and the rest) otherwise shows on their skater for
|
||||
everyone.
|
||||
parks Layout for each park lot, e.g. "skatepark_01", or "empty".
|
||||
world_layer_sync Force the "layers" below on every player.
|
||||
layers World layer key -> "on" / "off".
|
||||
admins SteamID64s (as strings) who may change settings in-game.
|
||||
bans Players who can never join. Managed with ban / unban.
|
||||
The ReSkate team's own list is separate: see global_bans.
|
||||
bone_scale_limit How far a mod may resize part of a skater for the other
|
||||
players (a "big head" mod and the like): the most a bone may
|
||||
be scaled, 1 to 8. 1 (default) shows every skater at the
|
||||
game's own proportions; 0 is no limit. The player with the
|
||||
mod still sees it on their own screen. Console:
|
||||
bone-scale <1-8>|off.
|
||||
|
||||
"network" - How players connect and how much they are sent. The defaults suit most servers.
|
||||
use_steam_relay How players reach the server: true (default) or false.
|
||||
true Through Steam's relay network. Nothing to open, and
|
||||
the server never sees a player's address. The route
|
||||
is Steam's choice and can be a long way round.
|
||||
false Straight to this server's "port" over UDP, which
|
||||
must be open to the internet. The shortest route,
|
||||
so the lowest ping. The server sees the addresses
|
||||
of the players who connect this way, as any
|
||||
dedicated server does; players never see each
|
||||
other's.
|
||||
With false the server still answers through the relays: a
|
||||
game tries the port first and uses the relays if it gets no
|
||||
answer in a few seconds, and a player can turn direct
|
||||
connections off in their own settings. So false never
|
||||
keeps anyone out. Steam vouches for who each player is
|
||||
either way. "net" shows who is direct, and the log has a
|
||||
[direct] line for each player who asks to connect that way.
|
||||
send_rate The most the server sends one player, in KB/s (default 900,
|
||||
128-16384). About 1100 is what reaches a player through
|
||||
Steam's relays: set higher, what is lost is resent until the
|
||||
connection is full of resends and half of everything is
|
||||
lost. Console: rate <KB/s>, also for the players already on.
|
||||
crowd_budget The most position updates a second one player is sent
|
||||
(default 600, 0 for no limit). Players near each other are
|
||||
sent at the full rate, 20 a second; this only matters once
|
||||
more are in one place than budget / 20, about 30. Then the
|
||||
nearest stay at the full rate and the farthest of the crowd
|
||||
drop to 10 and 5 a second, instead of everyone's connection
|
||||
filling up. About 1000 is what reaches a player through
|
||||
Steam's relays; more than that was seen to lose half.
|
||||
Console: crowd <n>|off.
|
||||
pack_ms How long a message to a player may wait to go in the same
|
||||
packet as the next ones, in milliseconds (default 10, 0 to
|
||||
50). A full server sends each player hundreds of small
|
||||
messages a second, each in a packet of its own with its own
|
||||
headers; packed, the same updates take fewer packets, less
|
||||
bandwidth and less CPU. It adds up to that long to when an
|
||||
update arrives. Voice is never held back. 0 sends every
|
||||
message at once, as before. Takes effect on restart.
|
||||
finger_distance Past this many metres (default 25) a player's fingers are not
|
||||
sent moving: they stay as they were, and move again when the
|
||||
player is nearer. Fingers are nearly half of every position
|
||||
update and cannot be made out at that distance. 0 always
|
||||
sends them.
|
||||
distances When far-away players update less often (metres).
|
||||
steam_debug true logs what Steam's own networking says it is doing: each
|
||||
connection asked for, and what it refused or ignored and
|
||||
why. For finding out why players cannot connect; it is a
|
||||
lot of text, so turn it off again afterwards.
|
||||
|
||||
"votes" - What players may vote on.
|
||||
votes Player votes, each off until turned on:
|
||||
"map": {"enabled": true, "percent": 60} /vote map <map>
|
||||
(a map in maps.pool)
|
||||
"kick": {"enabled": true, "percent": 60} /vote kick <player>
|
||||
"time_of_day": {"enabled": true, "percent": 50} /vote tod <time>
|
||||
(needs world_layer_sync)
|
||||
"percent" is the share of connected players whose yes passes
|
||||
it. "seconds" (default 30) is how long a vote runs and
|
||||
"cooldown_seconds" (default 60) how long a player waits before
|
||||
starting another. Players vote with /yes and /no in chat;
|
||||
admins cannot be vote-kicked.
|
||||
|
||||
Every change made from the console or by an admin is saved back to this file.
|
||||
|
||||
Bans are kept in data/bans.json, beside this file: the players who can never
|
||||
join, managed with ban / unban. The ReSkate team's own list is separate: see
|
||||
use_global_bans. A config from an older version that still has "bans" in it has
|
||||
them moved there the first time the server starts.
|
||||
|
||||
Console and admin commands
|
||||
--------------------------
|
||||
Type these in the server window. Admins run the same commands in the game with
|
||||
@@ -220,8 +233,7 @@ and change voice, distances, placement and kicks from the Multiplayer menu.
|
||||
help A short list of every command.
|
||||
status Name, map, players, code.
|
||||
players Connected players and their SteamID64s.
|
||||
reserved Who has a reserved slot and how many are kept.
|
||||
reserved slots <n> Keep n slots for them (console only), 0 for none.
|
||||
reserved Who has a reserved slot.
|
||||
reserved add|remove <player or id> (console only)
|
||||
net [player] How the connections are doing right now: traffic,
|
||||
queues, the server's own loop timing, and the twelve
|
||||
@@ -241,8 +253,8 @@ and change voice, distances, placement and kicks from the Multiplayer menu.
|
||||
map <name> e.g. map San Vansterdam, map grom, map bbcity
|
||||
maps The maps this server knows.
|
||||
map-pool [add|remove <map>|clear] The maps players vote between and the
|
||||
rotation uses (see map_pool).
|
||||
rotation [<minutes>|off] Change the map on a timer (see map_rotation_minutes).
|
||||
rotation uses (see maps.pool).
|
||||
rotation [<minutes>|off] Change the map on a timer (see maps.rotation_minutes).
|
||||
name <text> password <text|off> welcome <text|off> listed on|off
|
||||
voice on|off voice-range <m>
|
||||
distances <full> <half> <half-return> <low>
|
||||
|
||||
@@ -35,10 +35,12 @@ int run() {
|
||||
std::vector<std::string> added;
|
||||
const auto config = load_config(file, &added);
|
||||
const auto has = [&](std::string_view name) { return std::ranges::find(added, name) != added.end(); };
|
||||
check(has("enforce_tuning") && has("votes.seconds") && has("votes.kick") && has("score_check") && has("score_allow") &&
|
||||
has("global_bans") && has("map_pool") && has("map_rotation_minutes"),
|
||||
check(has("anti_cheat.enforce_tuning") && has("votes.seconds") && has("votes.kick") && has("anti_cheat.modified_scoring") && has("anti_cheat.allowed_scoring_mods") &&
|
||||
has("access.use_global_bans") && has("maps.pool") && has("maps.rotation_minutes"),
|
||||
"New settings not reported");
|
||||
check(!has("name") && !has("tps") && !has("votes.map"), "Settings the file had reported as new");
|
||||
// A setting under its old name is read, written back under its new one, and not called new.
|
||||
check(!has("players.allow_boosts") && has("players.allow_noclip"), "A renamed setting was reported as new, or a new one was not");
|
||||
check(!has("server.name") && !has("tps") && !has("votes.map"), "Settings the file had reported as new");
|
||||
const auto written = text(file);
|
||||
check(written.find("\"enforce_tuning\"") != std::string::npos && written.find("\"seconds\"") != std::string::npos,
|
||||
"New settings not written into the file");
|
||||
@@ -51,7 +53,7 @@ int run() {
|
||||
// steam_token: empty (anonymous) unless set, kept on a rewrite, and only letters and digits.
|
||||
// (The maps are not loaded yet, so config_error has another complaint: look for this one.)
|
||||
const auto token_refused = [](const ServerConfig &c) { return config_error(c).find("steam_token") != std::string::npos; };
|
||||
check(has("steam_token") && config.steam_token.empty() && !token_refused(config), "steam_token is not a new, empty setting");
|
||||
check(has("server.steam_token") && config.steam_token.empty() && !token_refused(config), "steam_token is not a new, empty setting");
|
||||
auto tokened = reloaded;
|
||||
tokened.steam_token = "0123456789ABCDEF0123456789ABCDEF";
|
||||
save_config(tokened);
|
||||
@@ -61,56 +63,52 @@ int run() {
|
||||
tokened.steam_token = std::string(65, 'A');
|
||||
check(token_refused(tokened), "An overlong steam_token was accepted");
|
||||
{
|
||||
check(has("crowd_budget") && config.crowd_budget == 600, "crowd_budget is not a new setting of 600");
|
||||
check(has("send_rate") && config.send_rate == 900, "send_rate is not a new setting of 900");
|
||||
check(has("network.crowd_budget") && config.crowd_budget == 600, "crowd_budget is not a new setting of 600");
|
||||
check(has("network.send_rate") && config.send_rate == 900, "send_rate is not a new setting of 900");
|
||||
ServerConfig rate;
|
||||
rate.send_rate = 64;
|
||||
check(config_error(rate).find("send_rate") != std::string::npos, "A send rate too low to play with was accepted");
|
||||
check(has("bone_scale_limit") && config.bone_scale_limit == 1, "bone_scale_limit is not a new setting of 1");
|
||||
check(has("anti_cheat.bone_scale_limit") && config.bone_scale_limit == 1, "bone_scale_limit is not a new setting of 1");
|
||||
ServerConfig scaled;
|
||||
scaled.bone_scale_limit = 0.5f;
|
||||
check(config_error(scaled).find("bone_scale_limit") != std::string::npos, "A bone scale limit under 1 was accepted");
|
||||
check(has("pack_ms") && config.pack_ms == 10, "pack_ms is not a new setting of 10");
|
||||
check(has("finger_distance") && config.finger_distance == 25, "finger_distance is not a new setting of 25");
|
||||
check(has("connection") && config.connection == "relay", "connection is not a new setting of relay");
|
||||
ServerConfig how;
|
||||
how.connection = "carrier pigeon";
|
||||
check(config_error(how).find("connection") != std::string::npos, "An unknown connection was accepted");
|
||||
check(has("network.pack_ms") && config.pack_ms == 10, "pack_ms is not a new setting of 10");
|
||||
check(has("network.finger_distance") && config.finger_distance == 25, "finger_distance is not a new setting of 25");
|
||||
check(has("network.use_steam_relay") && config.use_steam_relay, "use_steam_relay is not a new setting of true");
|
||||
ServerConfig crowd;
|
||||
crowd.crowd_budget = 50;
|
||||
check(config_error(crowd).find("crowd_budget") != std::string::npos, "A crowd budget too small to play with was accepted");
|
||||
}
|
||||
// Reserved slots: none unless set; the last of them only the listed players and admins take.
|
||||
{
|
||||
check(has("reserved_slots") && has("reserved") && config.reserved_slots == 0 && config.reserved.empty(),
|
||||
"Reserved slots are not new settings that reserve nothing");
|
||||
check(has("access.reserved_players_slots") && config.reserved.empty(), "Reserved slots are not a new setting that reserves nothing");
|
||||
ServerConfig slots;
|
||||
slots.max_players = 4;
|
||||
const std::uint64_t vip = 76561198000000001ULL, admin = 76561198000000002ULL, anyone = 76561198000000003ULL;
|
||||
check(may_join(slots, anyone, 3) && !may_join(slots, anyone, 4) && !may_join(slots, vip, 4), "A server with nothing reserved did not fill up plainly");
|
||||
slots.reserved_slots = 2;
|
||||
slots.reserved = {vip};
|
||||
check(may_join(slots, anyone, 3, 0) && !may_join(slots, anyone, 4, 0) && !may_join(slots, vip, 4, 0), "A server with nothing reserved did not fill up plainly");
|
||||
// A slot for each reserved player who is not on; one who is on has taken theirs.
|
||||
const std::uint64_t vip2 = 76561198000000004ULL;
|
||||
slots.reserved = {vip, vip2};
|
||||
slots.admins = {admin};
|
||||
check(may_join(slots, anyone, 1) && !may_join(slots, anyone, 2) && !may_join(slots, anyone, 3), "Reserved slots were given to anyone");
|
||||
check(may_join(slots, vip, 2) && may_join(slots, vip, 3) && may_join(slots, admin, 3), "A reserved player or an admin was kept out of a reserved slot");
|
||||
check(!may_join(slots, vip, 4) && !may_join(slots, admin, 4), "A full server let someone in");
|
||||
check(may_join(slots, anyone, 1, 0) && !may_join(slots, anyone, 2, 0) && !may_join(slots, anyone, 3, 0), "Reserved slots were given to anyone");
|
||||
check(may_join(slots, anyone, 2, 1) && !may_join(slots, anyone, 3, 1) && may_join(slots, anyone, 3, 2), "A slot was kept for a reserved player who is already on");
|
||||
check(may_join(slots, vip, 2, 0) && may_join(slots, vip, 3, 1) && may_join(slots, admin, 3, 0), "A reserved player or an admin was kept out of a reserved slot");
|
||||
check(!may_join(slots, vip, 4, 1) && !may_join(slots, admin, 4, 2), "A full server let someone in");
|
||||
const auto refused = [](const ServerConfig &c) { return config_error(c).find("reserved") != std::string::npos; };
|
||||
check(!refused(slots), "Valid reserved slots were refused");
|
||||
slots.reserved_slots = 4;
|
||||
slots.reserved = {vip, vip2, 76561198000000005ULL, 76561198000000006ULL};
|
||||
check(refused(slots), "A server with every slot reserved was accepted");
|
||||
slots.reserved_slots = 2;
|
||||
slots.reserved.push_back(42);
|
||||
slots.reserved = {vip, 42};
|
||||
check(refused(slots), "A reserved entry that is not a player was accepted");
|
||||
auto kept = reloaded;
|
||||
kept.reserved_slots = 3;
|
||||
kept.reserved = {vip};
|
||||
save_config(kept);
|
||||
const auto back = load_config(file);
|
||||
check(back.reserved_slots == 3 && back.reserved == std::vector<std::uint64_t>{vip}, "Reserved slots were not kept");
|
||||
check(back.reserved == std::vector<std::uint64_t>{vip} && text(file).find("\"reserved_slots\"") == std::string::npos, "Reserved slots were not kept");
|
||||
save_config(reloaded);
|
||||
}
|
||||
// object_limit: 100 unless set (0 is no limit), and kept on a rewrite.
|
||||
check(has("object_limit") && config.object_limit == 100, "object_limit is not a new setting of 100");
|
||||
check(has("players.object_limit") && config.object_limit == 100, "object_limit is not a new setting of 100");
|
||||
auto limited = reloaded;
|
||||
limited.object_limit = 50;
|
||||
save_config(limited);
|
||||
@@ -152,6 +150,66 @@ int run() {
|
||||
const auto odd = load_config(file);
|
||||
check(odd.score_check == "warn" && odd.score_allow == std::vector<std::uint64_t>{0xabc}, "Bad scoring settings not cleaned");
|
||||
|
||||
// A config from before the sections: every setting is read from where it was, under the name
|
||||
// it had, and the file is written back in sections, in reading order, with nothing called new
|
||||
// that was only moved.
|
||||
{
|
||||
std::ofstream(file, std::ios::binary) << R"({"name": "Flat", "no_bail": false, "score_check": "kick", "score_allow": ["abc"],
|
||||
"speed_check": "off", "map": "Isle of Grom", "map_pool": ["Isle of Grom"], "map_rotation_minutes": 30, "welcome": "Hi",
|
||||
"admins": ["76561198000000002"], "reserved": ["76561198000000001"], "reserved_slots": 2, "tps": 30, "global_bans": false,
|
||||
"voice_chat": false, "send_rate": 700, "distances": {"half_rate_start": 70}, "parks": {"financial": "empty"},
|
||||
"votes": {"map": {"enabled": false, "percent": 40}, "kick": {"enabled": true, "percent": 60},
|
||||
"time_of_day": {"enabled": true, "percent": 50}, "seconds": 30, "cooldown_seconds": 60}})";
|
||||
std::vector<std::string> fresh;
|
||||
const auto flat = load_config(file, &fresh);
|
||||
check(flat.name == "Flat" && !flat.no_bail && flat.score_check == "kick" && flat.score_allow == std::vector<std::uint64_t>{0xabc} &&
|
||||
flat.speed_check == "off" && flat.map == "Isle of Grom" && flat.map_pool.size() == 1 && flat.map_rotation == 30 && flat.welcome == "Hi" &&
|
||||
flat.admins.size() == 1 && flat.reserved.size() == 1 && !flat.global_bans && !flat.voice_chat && flat.send_rate == 700 &&
|
||||
flat.distances.half_rate_start == 70 && !flat.votes.map.enabled && flat.votes.map.percent == 40,
|
||||
"A config from before the sections lost a setting");
|
||||
const auto fresh_has = [&](std::string_view name) { return std::ranges::find(fresh, name) != fresh.end(); };
|
||||
check(!fresh_has("players.allow_no_bail") && !fresh_has("anti_cheat.modified_scoring") && !fresh_has("maps.pool") && !fresh_has("server.name") &&
|
||||
fresh_has("network.pack_ms"),
|
||||
"A moved setting was called new, or a new one was not");
|
||||
const auto now = text(file);
|
||||
const auto at = [&](std::string_view what) { return now.find(what); };
|
||||
check(at("\"server\"") < at("\"access\"") && at("\"access\"") < at("\"maps\"") && at("\"maps\"") < at("\"players\"") &&
|
||||
at("\"players\"") < at("\"anti_cheat\"") && at("\"anti_cheat\"") < at("\"network\"") && at("\"network\"") < at("\"votes\"") &&
|
||||
at("\"votes\"") != std::string::npos && at("\"name\"") < at("\"password\"") && at("\"password\"") < at("\"welcome_message\""),
|
||||
"The config was not written in sections, in reading order");
|
||||
check(at("\"no_bail\"") == std::string::npos && at("\"tps\"") == std::string::npos && at("\"score_check\"") == std::string::npos &&
|
||||
at("\"reserved_slots\"") == std::string::npos && at("\"modified_scoring\": \"kick\"") != std::string::npos,
|
||||
"Old names were left in the config");
|
||||
std::vector<std::string> again;
|
||||
const auto back = load_config(file, &again);
|
||||
check(again.empty() && back.name == "Flat" && !back.no_bail && back.score_check == "kick" && back.map_pool.size() == 1 && back.reserved.size() == 1 &&
|
||||
back.distances.half_rate_start == 70 && !back.votes.map.enabled && text(file) == now,
|
||||
"A config in sections did not read back as it was written");
|
||||
std::filesystem::remove_all(folder / "data");
|
||||
}
|
||||
// Bans have a file of their own; a config that still holds them has them moved there.
|
||||
{
|
||||
std::filesystem::remove_all(folder / "data");
|
||||
std::ofstream(file, std::ios::binary) << R"({"name": "Old", "bans": [{"id": "76561198000000001", "name": "A", "added": 5}]})";
|
||||
auto old = load_config(file);
|
||||
check(old.bans.size() == 1 && old.bans[0].id == 76561198000000001ULL && old.bans[0].name == "A", "An old config's bans were not read");
|
||||
std::stringstream now;
|
||||
now << std::ifstream(file, std::ios::binary).rdbuf();
|
||||
check(std::filesystem::exists(bans_file(old)) && now.str().find("\"bans\"") == std::string::npos, "Bans were not moved to data/bans.json");
|
||||
old.bans.push_back({76561198000000002ULL, "B", 6});
|
||||
save_config(old);
|
||||
const auto again = load_config(file);
|
||||
check(again.bans.size() == 2 && again.bans[1].name == "B" && again.bans[1].added == 6, "Bans were not kept in their own file");
|
||||
std::ofstream(bans_file(old), std::ios::binary) << "{not a list";
|
||||
bool refused_bans{};
|
||||
try {
|
||||
static_cast<void>(load_config(file));
|
||||
} catch (const std::exception &) {
|
||||
refused_bans = true;
|
||||
}
|
||||
check(refused_bans, "A server started without its bans");
|
||||
std::filesystem::remove_all(folder / "data");
|
||||
}
|
||||
// A port outside 1-65535 is refused, not wrapped to another port, and the file keeps the typo.
|
||||
const auto refused = [&](const char *json) {
|
||||
std::ofstream(file, std::ios::binary) << json;
|
||||
|
||||
+2
-2
@@ -482,7 +482,7 @@ int run(int argc, char **argv, bool skip_update) {
|
||||
std::future<BanListCheck> ban_check;
|
||||
auto next_ban_check = next_advertise;
|
||||
bool bans_unread{};
|
||||
if (!config.global_bans) write_log("Global bans are off (\"global_bans\": false): only this server's own bans apply.");
|
||||
if (!config.global_bans) write_log("Global bans are off (\"use_global_bans\": false): only this server's own bans apply.");
|
||||
// Signed in to Steam, as last logged, and when it is looked at again. Players already on stay
|
||||
// connected through a lost sign-in, but nobody new can join: Steam carries the first messages
|
||||
// of a connection, and theirs time out ("negotiate rendezvous").
|
||||
@@ -592,7 +592,7 @@ int run(int argc, char **argv, bool skip_update) {
|
||||
name_allowed = allowed;
|
||||
}
|
||||
steam.advertise({config.name, host.map_name(), host.players(), config.max_players, !config.password.empty(),
|
||||
config.listed && allowed, host.secret(), host.direct_port(), host.direct_address()});
|
||||
config.listed && allowed, host.secret(), host.direct_port()});
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
}
|
||||
|
||||
+277
-185
@@ -4,6 +4,8 @@
|
||||
#include "Extension/Multiplayer/Net/protocol.h"
|
||||
#include "Engine/Game/World/world_names.h"
|
||||
#include <algorithm>
|
||||
#include <initializer_list>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
@@ -14,85 +16,130 @@ namespace {
|
||||
std::string placement_text(ObjectPlacement policy) {
|
||||
return policy == ObjectPlacement::everyone ? "everyone" : policy == ObjectPlacement::host_only ? "admins" : "nobody";
|
||||
}
|
||||
Json to_json(const ServerConfig &c) {
|
||||
auto root = Json::object();
|
||||
root["name"] = c.name;
|
||||
root["map"] = c.map;
|
||||
// The config file as it is written: sections, and the settings in each, in the order an owner
|
||||
// reads them (a Json object alone would put them in alphabetical order).
|
||||
struct Layout {
|
||||
std::vector<std::string> names;
|
||||
std::vector<Layout> members;
|
||||
Json value;
|
||||
bool leaf{};
|
||||
Layout §ion(std::string name) {
|
||||
names.push_back(std::move(name));
|
||||
return members.emplace_back();
|
||||
}
|
||||
void set(std::string name, Json json) {
|
||||
names.push_back(std::move(name));
|
||||
auto &member = members.emplace_back();
|
||||
member.value = std::move(json);
|
||||
member.leaf = true;
|
||||
}
|
||||
};
|
||||
void write(std::string &out, const Layout &layout, std::size_t depth) {
|
||||
const std::string inner(2 * (depth + 1), ' ');
|
||||
out += "{\n";
|
||||
for (std::size_t i = 0; i < layout.names.size(); ++i) {
|
||||
out += inner + Json(layout.names[i]).dump() + ": ";
|
||||
if (layout.members[i].leaf) {
|
||||
// A list or an object of its own spans lines: each goes in by this setting's depth.
|
||||
for (const char c : layout.members[i].value.dump(2)) {
|
||||
out += c;
|
||||
if (c == '\n') out += inner;
|
||||
}
|
||||
} else {
|
||||
write(out, layout.members[i], depth + 1);
|
||||
}
|
||||
out += i + 1 < layout.names.size() ? ",\n" : "\n";
|
||||
}
|
||||
out += std::string(2 * depth, ' ') + "}";
|
||||
}
|
||||
Layout layout(const ServerConfig &c) {
|
||||
// SteamID64s are written as strings: JSON readers often lose 64-bit precision.
|
||||
const auto ids = [](const std::vector<std::uint64_t> &list) {
|
||||
auto out = Json::array();
|
||||
for (const auto id : list) out.push_back(std::to_string(id));
|
||||
return out;
|
||||
};
|
||||
Layout root;
|
||||
auto &server = root.section("server");
|
||||
server.set("name", c.name);
|
||||
server.set("password", c.password);
|
||||
server.set("welcome_message", c.welcome);
|
||||
server.set("listed", c.listed);
|
||||
server.set("max_players", c.max_players);
|
||||
server.set("port", static_cast<unsigned>(c.port));
|
||||
server.set("query_port", static_cast<unsigned>(c.query_port));
|
||||
server.set("steam_token", c.steam_token);
|
||||
server.set("auto_update", c.auto_update);
|
||||
server.set("activity_log", c.activity_log);
|
||||
|
||||
auto &access = root.section("access");
|
||||
access.set("admins", ids(c.admins));
|
||||
access.set("reserved_players_slots", ids(c.reserved));
|
||||
access.set("use_global_bans", c.global_bans);
|
||||
|
||||
auto &maps = root.section("maps");
|
||||
maps.set("map", c.map);
|
||||
auto pool = Json::array();
|
||||
for (const auto &map : c.map_pool) pool.push_back(map);
|
||||
root["map_pool"] = std::move(pool);
|
||||
root["map_rotation_minutes"] = c.map_rotation;
|
||||
root["max_players"] = c.max_players;
|
||||
root["reserved_slots"] = c.reserved_slots;
|
||||
root["crowd_budget"] = c.crowd_budget;
|
||||
root["bone_scale_limit"] = c.bone_scale_limit;
|
||||
root["connection"] = c.connection;
|
||||
root["direct_address"] = c.direct_address;
|
||||
root["steam_debug"] = c.steam_debug;
|
||||
root["relay_everything"] = c.relay_everything;
|
||||
root["direct_port"] = c.direct_port;
|
||||
root["pack_ms"] = c.pack_ms;
|
||||
root["finger_distance"] = c.finger_distance;
|
||||
root["send_rate"] = c.send_rate;
|
||||
auto reserved = Json::array();
|
||||
for (const auto id : c.reserved) reserved.push_back(std::to_string(id)); // as strings, like the admins
|
||||
root["reserved"] = std::move(reserved);
|
||||
root["password"] = c.password;
|
||||
root["welcome"] = c.welcome;
|
||||
root["listed"] = c.listed;
|
||||
root["steam_token"] = c.steam_token;
|
||||
root["auto_update"] = c.auto_update;
|
||||
root["global_bans"] = c.global_bans;
|
||||
root["activity_log"] = c.activity_log;
|
||||
root["announce_throwdowns"] = c.announce_throwdowns;
|
||||
root["parties"] = c.parties;
|
||||
root["party_size"] = c.party_size;
|
||||
root["speed_check"] = c.speed_check;
|
||||
root["score_check"] = c.score_check;
|
||||
auto allowed = Json::array();
|
||||
for (const auto fingerprint : c.score_allow) allowed.push_back(scoring_text(fingerprint));
|
||||
root["score_allow"] = std::move(allowed);
|
||||
root["port"] = static_cast<unsigned>(c.port);
|
||||
root["query_port"] = static_cast<unsigned>(c.query_port);
|
||||
root["tps"] = c.tps;
|
||||
root["voice_chat"] = c.voice_chat;
|
||||
root["voice_range"] = static_cast<double>(c.voice_range);
|
||||
auto distances = Json::object();
|
||||
distances["full_rate_return"] = c.distances.full_rate_return;
|
||||
distances["half_rate_start"] = c.distances.half_rate_start;
|
||||
distances["half_rate_return"] = c.distances.half_rate_return;
|
||||
distances["low_rate_start"] = c.distances.low_rate_start;
|
||||
root["distances"] = std::move(distances);
|
||||
root["object_placement"] = placement_text(c.object_placement);
|
||||
root["object_limit"] = c.object_limit;
|
||||
root["noclip"] = c.noclip;
|
||||
root["no_bail"] = c.no_bail;
|
||||
root["boosts"] = c.boosts;
|
||||
root["enforce_tuning"] = c.enforce_tuning;
|
||||
auto votes = Json::object();
|
||||
const auto vote = [](const VoteSetting &v) {
|
||||
auto item = Json::object();
|
||||
item["enabled"] = v.enabled;
|
||||
item["percent"] = v.percent;
|
||||
return item;
|
||||
};
|
||||
votes["map"] = vote(c.votes.map);
|
||||
votes["kick"] = vote(c.votes.kick);
|
||||
votes["time_of_day"] = vote(c.votes.time);
|
||||
votes["seconds"] = c.votes.seconds;
|
||||
votes["cooldown_seconds"] = c.votes.cooldown;
|
||||
root["votes"] = std::move(votes);
|
||||
maps.set("pool", std::move(pool));
|
||||
maps.set("rotation_minutes", c.map_rotation);
|
||||
auto parks = Json::object();
|
||||
for (unsigned lot = 0; lot < park_lots.size(); ++lot) parks[park_lots[lot].key] = c.parks[lot];
|
||||
root["parks"] = std::move(parks);
|
||||
root["world_layer_sync"] = c.world_layer_sync;
|
||||
maps.set("parks", std::move(parks));
|
||||
maps.set("world_layer_sync", c.world_layer_sync);
|
||||
auto layers = Json::object();
|
||||
for (const auto &[key, mode] : c.layers) layers[key] = mode;
|
||||
root["layers"] = std::move(layers);
|
||||
// SteamID64s are written as strings: JSON readers often lose 64-bit precision.
|
||||
auto admins = Json::array();
|
||||
for (const auto id : c.admins) admins.push_back(std::to_string(id));
|
||||
root["admins"] = std::move(admins);
|
||||
maps.set("layers", std::move(layers));
|
||||
|
||||
auto &players = root.section("players");
|
||||
players.set("allow_boosts", c.boosts);
|
||||
players.set("allow_no_bail", c.no_bail);
|
||||
players.set("allow_noclip", c.noclip);
|
||||
players.set("allow_parties", c.parties);
|
||||
players.set("party_size", c.party_size);
|
||||
players.set("allow_voice_chat", c.voice_chat);
|
||||
players.set("voice_range", static_cast<double>(c.voice_range));
|
||||
players.set("object_placement", placement_text(c.object_placement));
|
||||
players.set("object_limit", c.object_limit);
|
||||
players.set("announce_throwdowns", c.announce_throwdowns);
|
||||
|
||||
auto &anti_cheat = root.section("anti_cheat");
|
||||
anti_cheat.set("speed_hack", c.speed_check);
|
||||
anti_cheat.set("modified_scoring", c.score_check);
|
||||
auto allowed = Json::array();
|
||||
for (const auto fingerprint : c.score_allow) allowed.push_back(scoring_text(fingerprint));
|
||||
anti_cheat.set("allowed_scoring_mods", std::move(allowed));
|
||||
anti_cheat.set("enforce_tuning", c.enforce_tuning);
|
||||
anti_cheat.set("bone_scale_limit", c.bone_scale_limit);
|
||||
|
||||
auto &network = root.section("network");
|
||||
network.set("use_steam_relay", c.use_steam_relay);
|
||||
network.set("send_rate", c.send_rate);
|
||||
network.set("crowd_budget", c.crowd_budget);
|
||||
network.set("pack_ms", c.pack_ms);
|
||||
network.set("finger_distance", c.finger_distance);
|
||||
auto &distances = network.section("distances");
|
||||
distances.set("full_rate_return", c.distances.full_rate_return);
|
||||
distances.set("half_rate_start", c.distances.half_rate_start);
|
||||
distances.set("half_rate_return", c.distances.half_rate_return);
|
||||
distances.set("low_rate_start", c.distances.low_rate_start);
|
||||
network.set("steam_debug", c.steam_debug);
|
||||
|
||||
auto &votes = root.section("votes");
|
||||
const auto vote = [&](const char *name, const VoteSetting &v) {
|
||||
auto &item = votes.section(name);
|
||||
item.set("enabled", v.enabled);
|
||||
item.set("percent", v.percent);
|
||||
};
|
||||
vote("map", c.votes.map);
|
||||
vote("kick", c.votes.kick);
|
||||
vote("time_of_day", c.votes.time);
|
||||
votes.set("seconds", c.votes.seconds);
|
||||
votes.set("cooldown_seconds", c.votes.cooldown);
|
||||
return root;
|
||||
}
|
||||
// Bans have a file of their own, beside the config.
|
||||
Json bans_json(const ServerConfig &c) {
|
||||
auto bans = Json::array();
|
||||
for (const auto &ban : c.bans) {
|
||||
auto row = Json::object();
|
||||
@@ -101,20 +148,22 @@ Json to_json(const ServerConfig &c) {
|
||||
row["added"] = ban.added;
|
||||
bans.push_back(std::move(row));
|
||||
}
|
||||
root["bans"] = std::move(bans);
|
||||
return root;
|
||||
return bans;
|
||||
}
|
||||
void write_file(const std::filesystem::path &file, const std::string &text) {
|
||||
if (file.has_parent_path()) std::filesystem::create_directories(file.parent_path());
|
||||
const auto temporary = std::filesystem::path(file).concat(".tmp");
|
||||
{
|
||||
std::ofstream out(temporary, std::ios::binary | std::ios::trunc);
|
||||
out << text << '\n';
|
||||
if (!out) throw std::runtime_error("Cannot write " + path_utf8(temporary));
|
||||
}
|
||||
std::filesystem::rename(temporary, file);
|
||||
}
|
||||
std::uint64_t steam_id(const Json &value) {
|
||||
if (value.is_string()) return std::stoull(value.string());
|
||||
return value.get<std::uint64_t>();
|
||||
}
|
||||
// The settings `expected` has that `file` lacks, nested objects included.
|
||||
void missing_settings(const Json &file, const Json &expected, const std::string &prefix, std::vector<std::string> &out) {
|
||||
for (const auto &[key, value] : expected.items()) {
|
||||
if (!file.contains(key)) out.push_back(prefix + key);
|
||||
else if (value.is_object() && file.at(key).is_object()) missing_settings(file.at(key), value, prefix + key + ".", out);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
ServerConfig load_config(const std::filesystem::path &file, std::vector<std::string> *added) {
|
||||
ServerConfig c;
|
||||
@@ -131,118 +180,162 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector<std::str
|
||||
}
|
||||
const auto root = Json::parse(text.str());
|
||||
if (!root.is_object()) throw std::runtime_error("The server config must be a JSON object.");
|
||||
c.name = root.value("name", c.name);
|
||||
c.map = root.value("map", c.map);
|
||||
if (root.contains("map_pool") && root.at("map_pool").is_array())
|
||||
for (const auto &map : root.at("map_pool"))
|
||||
if (map.is_string() && !map.string().empty()) c.map_pool.push_back(map.string());
|
||||
c.map_rotation = std::min(root.value("map_rotation_minutes", c.map_rotation), max_map_rotation);
|
||||
c.max_players = root.value("max_players", c.max_players);
|
||||
c.reserved_slots = root.value("reserved_slots", c.reserved_slots);
|
||||
c.crowd_budget = root.value("crowd_budget", c.crowd_budget);
|
||||
c.bone_scale_limit = root.value("bone_scale_limit", c.bone_scale_limit);
|
||||
c.connection = root.value("connection", c.connection);
|
||||
c.direct_address = root.value("direct_address", c.direct_address);
|
||||
c.steam_debug = root.value("steam_debug", c.steam_debug);
|
||||
c.relay_everything = root.value("relay_everything", c.relay_everything);
|
||||
c.direct_port = root.value("direct_port", c.direct_port);
|
||||
c.pack_ms = root.value("pack_ms", c.pack_ms);
|
||||
c.finger_distance = root.value("finger_distance", c.finger_distance);
|
||||
c.send_rate = root.value("send_rate", c.send_rate);
|
||||
if (root.contains("reserved") && root.at("reserved").is_array())
|
||||
for (const auto &id : root.at("reserved")) c.reserved.push_back(steam_id(id));
|
||||
c.password = root.value("password", c.password);
|
||||
c.welcome = root.value("welcome", c.welcome);
|
||||
c.listed = root.value("listed", c.listed);
|
||||
c.steam_token = root.value("steam_token", c.steam_token);
|
||||
c.auto_update = root.value("auto_update", c.auto_update);
|
||||
c.global_bans = root.value("global_bans", c.global_bans);
|
||||
c.activity_log = root.value("activity_log", c.activity_log);
|
||||
c.announce_throwdowns = root.value("announce_throwdowns", c.announce_throwdowns);
|
||||
c.parties = root.value("parties", c.parties);
|
||||
c.party_size = std::clamp(root.value("party_size", c.party_size), 2U, 8U);
|
||||
c.speed_check = root.value("speed_check", c.speed_check);
|
||||
if (c.speed_check != "off" && c.speed_check != "warn" && c.speed_check != "kick") c.speed_check = "warn";
|
||||
c.score_check = root.value("score_check", c.score_check);
|
||||
if (c.score_check != "off" && c.score_check != "warn" && c.score_check != "kick") c.score_check = "warn";
|
||||
if (root.contains("score_allow") && root.at("score_allow").is_array())
|
||||
for (const auto &value : root.at("score_allow"))
|
||||
if (value.is_string())
|
||||
if (const auto fingerprint = parse_scoring(value.string()))
|
||||
c.score_allow.push_back(*fingerprint);
|
||||
// A setting is in its section under its name. A config from before the sections has it at the
|
||||
// top, under that name or an older one: it is read from there, and the file is written back
|
||||
// as it is laid out now. A setting found nowhere is new, and the owner is told of it.
|
||||
std::vector<std::string> missing;
|
||||
bool moved{};
|
||||
using Old = std::initializer_list<std::string_view>;
|
||||
const auto find = [&](std::string_view section, std::string_view key, Old old = {}) -> const Json * {
|
||||
if (root.contains(section) && root.at(section).is_object() && root.at(section).contains(key)) return &root.at(section).at(key);
|
||||
if (root.contains(key) && !root.at(key).is_null() && key != "votes") {
|
||||
moved = true;
|
||||
return &root.at(key);
|
||||
}
|
||||
for (const auto name : old)
|
||||
if (root.contains(name)) {
|
||||
moved = true;
|
||||
return &root.at(name);
|
||||
}
|
||||
missing.push_back(std::string(section) + "." + std::string(key));
|
||||
return nullptr;
|
||||
};
|
||||
const auto get = [&]<typename T>(std::string_view section, std::string_view key, T fallback, Old old = {}) {
|
||||
const auto *found = find(section, key, old);
|
||||
return found ? found->template get<T>() : fallback;
|
||||
};
|
||||
c.name = get("server", "name", c.name);
|
||||
c.password = get("server", "password", c.password);
|
||||
c.welcome = get("server", "welcome_message", c.welcome, {"welcome"});
|
||||
c.listed = get("server", "listed", c.listed);
|
||||
c.max_players = get("server", "max_players", c.max_players);
|
||||
// Checked here, not in config_error: once narrowed, 70000 is just port 4464, and the
|
||||
// next save would write that over the owner's typo.
|
||||
const auto read_port = [&](const char *key, std::uint16_t fallback) {
|
||||
const auto value = root.value(key, static_cast<unsigned>(fallback));
|
||||
const auto value = get("server", key, static_cast<unsigned>(fallback));
|
||||
if (value < 1 || value > 65535) throw std::runtime_error(std::string(key) + " must be 1 to 65535.");
|
||||
return static_cast<std::uint16_t>(value);
|
||||
};
|
||||
c.port = read_port("port", c.port);
|
||||
c.query_port = read_port("query_port", c.query_port);
|
||||
c.tps = dedicated_tps; // not the file's (see ServerConfig::tps)
|
||||
c.voice_chat = root.value("voice_chat", c.voice_chat);
|
||||
c.voice_range = root.value("voice_range", c.voice_range);
|
||||
if (root.contains("distances")) {
|
||||
const auto &d = root.at("distances");
|
||||
c.distances.full_rate_return = d.value("full_rate_return", c.distances.full_rate_return);
|
||||
c.distances.half_rate_start = d.value("half_rate_start", c.distances.half_rate_start);
|
||||
c.distances.half_rate_return = d.value("half_rate_return", c.distances.half_rate_return);
|
||||
c.distances.low_rate_start = d.value("low_rate_start", c.distances.low_rate_start);
|
||||
}
|
||||
c.noclip = root.value("noclip", c.noclip);
|
||||
c.no_bail = root.value("no_bail", c.no_bail);
|
||||
c.boosts = root.value("boosts", c.boosts);
|
||||
c.enforce_tuning = root.value("enforce_tuning", c.enforce_tuning);
|
||||
if (root.contains("votes") && root.at("votes").is_object()) {
|
||||
const auto &votes = root.at("votes");
|
||||
const auto read_vote = [&](const char *key, VoteSetting &v) {
|
||||
if (!votes.contains(key) || !votes.at(key).is_object()) return;
|
||||
const auto &item = votes.at(key);
|
||||
v.enabled = item.value("enabled", v.enabled);
|
||||
v.percent = std::clamp(item.value("percent", v.percent), 1U, 100U);
|
||||
};
|
||||
read_vote("map", c.votes.map);
|
||||
read_vote("kick", c.votes.kick);
|
||||
read_vote("time_of_day", c.votes.time);
|
||||
c.votes.seconds = std::clamp(votes.value("seconds", c.votes.seconds), 10U, 300U);
|
||||
c.votes.cooldown = std::clamp(votes.value("cooldown_seconds", c.votes.cooldown), 0U, 3600U);
|
||||
}
|
||||
c.object_limit = root.value("object_limit", c.object_limit);
|
||||
const auto placement = root.value("object_placement", placement_text(c.object_placement));
|
||||
c.steam_token = get("server", "steam_token", c.steam_token);
|
||||
c.auto_update = get("server", "auto_update", c.auto_update);
|
||||
c.activity_log = get("server", "activity_log", c.activity_log);
|
||||
|
||||
if (const auto *admins = find("access", "admins"); admins && admins->is_array())
|
||||
for (const auto &id : *admins) c.admins.push_back(steam_id(id));
|
||||
if (const auto *reserved = find("access", "reserved_players_slots", {"reserved"}); reserved && reserved->is_array())
|
||||
for (const auto &id : *reserved) c.reserved.push_back(steam_id(id));
|
||||
c.global_bans = get("access", "use_global_bans", c.global_bans, {"global_bans"});
|
||||
|
||||
c.map = get("maps", "map", c.map);
|
||||
if (const auto *pool = find("maps", "pool", {"map_pool"}); pool && pool->is_array())
|
||||
for (const auto &map : *pool)
|
||||
if (map.is_string() && !map.string().empty()) c.map_pool.push_back(map.string());
|
||||
c.map_rotation = std::min(get("maps", "rotation_minutes", c.map_rotation, {"map_rotation_minutes"}), max_map_rotation);
|
||||
if (const auto *parks = find("maps", "parks"); parks && parks->is_object())
|
||||
for (unsigned lot = 0; lot < park_lots.size(); ++lot) c.parks[lot] = parks->value(park_lots[lot].key, c.parks[lot]);
|
||||
c.world_layer_sync = get("maps", "world_layer_sync", c.world_layer_sync);
|
||||
if (const auto *layers = find("maps", "layers"); layers && layers->is_object())
|
||||
for (const auto &[key, mode] : layers->items())
|
||||
if (mode.is_string()) c.layers[key] = mode.string();
|
||||
|
||||
c.boosts = get("players", "allow_boosts", c.boosts, {"boosts"});
|
||||
c.no_bail = get("players", "allow_no_bail", c.no_bail, {"no_bail"});
|
||||
c.noclip = get("players", "allow_noclip", c.noclip, {"noclip"});
|
||||
c.parties = get("players", "allow_parties", c.parties, {"parties"});
|
||||
c.party_size = std::clamp(get("players", "party_size", c.party_size), 2U, 8U);
|
||||
c.voice_chat = get("players", "allow_voice_chat", c.voice_chat, {"voice_chat"});
|
||||
c.voice_range = get("players", "voice_range", c.voice_range);
|
||||
const auto placement = get("players", "object_placement", placement_text(c.object_placement));
|
||||
// On a dedicated server the protocol's "host only" means its admins.
|
||||
c.object_placement = placement == "nobody" ? ObjectPlacement::nobody
|
||||
: placement == "admins" || placement == "host" ? ObjectPlacement::host_only : ObjectPlacement::everyone;
|
||||
if (root.contains("parks"))
|
||||
for (unsigned lot = 0; lot < park_lots.size(); ++lot)
|
||||
c.parks[lot] = root.at("parks").value(park_lots[lot].key, c.parks[lot]);
|
||||
c.world_layer_sync = root.value("world_layer_sync", c.world_layer_sync);
|
||||
if (root.contains("layers") && root.at("layers").is_object())
|
||||
for (const auto &[key, mode] : root.at("layers").items())
|
||||
if (mode.is_string()) c.layers[key] = mode.string();
|
||||
if (root.contains("admins"))
|
||||
for (const auto &id : root.at("admins")) c.admins.push_back(steam_id(id));
|
||||
if (root.contains("bans"))
|
||||
for (const auto &row : root.at("bans"))
|
||||
c.bans.push_back({steam_id(row.at("id")), row.value("name", std::string{}),
|
||||
row.value("added", std::int64_t{})});
|
||||
// A config from an older version: write the new settings into it so owners can see them.
|
||||
std::vector<std::string> missing;
|
||||
missing_settings(root, to_json(c), {}, missing);
|
||||
if (!missing.empty()) {
|
||||
c.object_limit = get("players", "object_limit", c.object_limit);
|
||||
c.announce_throwdowns = get("players", "announce_throwdowns", c.announce_throwdowns);
|
||||
|
||||
c.speed_check = get("anti_cheat", "speed_hack", c.speed_check, {"speed_check"});
|
||||
if (c.speed_check != "off" && c.speed_check != "warn" && c.speed_check != "kick") c.speed_check = "warn";
|
||||
c.score_check = get("anti_cheat", "modified_scoring", c.score_check, {"score_check"});
|
||||
if (c.score_check != "off" && c.score_check != "warn" && c.score_check != "kick") c.score_check = "warn";
|
||||
if (const auto *allowed = find("anti_cheat", "allowed_scoring_mods", {"score_allow"}); allowed && allowed->is_array())
|
||||
for (const auto &value : *allowed)
|
||||
if (value.is_string())
|
||||
if (const auto fingerprint = parse_scoring(value.string())) c.score_allow.push_back(*fingerprint);
|
||||
c.enforce_tuning = get("anti_cheat", "enforce_tuning", c.enforce_tuning);
|
||||
c.bone_scale_limit = get("anti_cheat", "bone_scale_limit", c.bone_scale_limit);
|
||||
|
||||
c.use_steam_relay = get("network", "use_steam_relay", c.use_steam_relay);
|
||||
c.send_rate = get("network", "send_rate", c.send_rate);
|
||||
c.crowd_budget = get("network", "crowd_budget", c.crowd_budget);
|
||||
c.pack_ms = get("network", "pack_ms", c.pack_ms);
|
||||
c.finger_distance = get("network", "finger_distance", c.finger_distance);
|
||||
if (const auto *d = find("network", "distances"); d && d->is_object()) {
|
||||
c.distances.full_rate_return = d->value("full_rate_return", c.distances.full_rate_return);
|
||||
c.distances.half_rate_start = d->value("half_rate_start", c.distances.half_rate_start);
|
||||
c.distances.half_rate_return = d->value("half_rate_return", c.distances.half_rate_return);
|
||||
c.distances.low_rate_start = d->value("low_rate_start", c.distances.low_rate_start);
|
||||
}
|
||||
c.steam_debug = get("network", "steam_debug", c.steam_debug);
|
||||
|
||||
// Votes were a section from the start.
|
||||
const Json no_votes = Json::object();
|
||||
const auto &votes = root.contains("votes") && root.at("votes").is_object() ? root.at("votes") : no_votes;
|
||||
const auto read_vote = [&](const char *key, VoteSetting &v) {
|
||||
if (!votes.contains(key) || !votes.at(key).is_object()) return missing.push_back(std::string("votes.") + key);
|
||||
const auto &item = votes.at(key);
|
||||
v.enabled = item.value("enabled", v.enabled);
|
||||
v.percent = std::clamp(item.value("percent", v.percent), 1U, 100U);
|
||||
};
|
||||
read_vote("map", c.votes.map);
|
||||
read_vote("kick", c.votes.kick);
|
||||
read_vote("time_of_day", c.votes.time);
|
||||
if (!votes.contains("seconds")) missing.push_back("votes.seconds");
|
||||
if (!votes.contains("cooldown_seconds")) missing.push_back("votes.cooldown_seconds");
|
||||
c.votes.seconds = std::clamp(votes.value("seconds", c.votes.seconds), 10U, 300U);
|
||||
c.votes.cooldown = std::clamp(votes.value("cooldown_seconds", c.votes.cooldown), 0U, 3600U);
|
||||
|
||||
const auto read_bans = [&](const Json &rows) {
|
||||
if (!rows.is_array()) throw std::runtime_error("The bans must be a JSON list.");
|
||||
for (const auto &row : rows) {
|
||||
const auto id = steam_id(row.at("id"));
|
||||
if (std::none_of(c.bans.begin(), c.bans.end(), [&](const auto &ban) { return ban.id == id; }))
|
||||
c.bans.push_back({id, row.value("name", std::string{}), row.value("added", std::int64_t{})});
|
||||
}
|
||||
};
|
||||
if (const auto bans = bans_file(c); std::filesystem::exists(bans)) {
|
||||
std::stringstream held;
|
||||
{
|
||||
std::ifstream in(bans, std::ios::binary);
|
||||
held << in.rdbuf();
|
||||
}
|
||||
try {
|
||||
read_bans(Json::parse(held.str()));
|
||||
} catch (const std::exception &e) {
|
||||
// Never started without its bans because their file has a typo in it.
|
||||
throw std::runtime_error("Cannot read " + path_utf8(bans) + ": " + e.what());
|
||||
}
|
||||
}
|
||||
// A config from an older version holds the bans itself: they are moved to their own file.
|
||||
const bool bans_moved = root.contains("bans");
|
||||
if (bans_moved) read_bans(root.at("bans"));
|
||||
// New settings, an older layout, or settings that are no longer any: the file is written
|
||||
// as it is now, so owners see what there is.
|
||||
const bool gone = root.contains("tps") || root.contains("reserved_slots") || root.contains("relay_everything") ||
|
||||
(root.contains("network") && root.at("network").contains("relay_everything"));
|
||||
if (!missing.empty() || moved || bans_moved || gone) {
|
||||
save_config(c);
|
||||
if (added) *added = std::move(missing);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
std::filesystem::path bans_file(const ServerConfig &c) { return c.file.parent_path() / "data" / "bans.json"; }
|
||||
void save_config(const ServerConfig &c) {
|
||||
if (c.file.has_parent_path()) std::filesystem::create_directories(c.file.parent_path());
|
||||
const auto temporary = std::filesystem::path(c.file).concat(".tmp");
|
||||
{
|
||||
std::ofstream out(temporary, std::ios::binary | std::ios::trunc);
|
||||
out << to_json(c).dump(2) << '\n';
|
||||
if (!out) throw std::runtime_error("Cannot write " + path_utf8(temporary));
|
||||
}
|
||||
std::filesystem::rename(temporary, c.file);
|
||||
// The bans first: a config that held them is only rewritten without them once they are safe.
|
||||
write_file(bans_file(c), bans_json(c).dump(2));
|
||||
std::string text;
|
||||
write(text, layout(c), 0);
|
||||
write_file(c.file, text);
|
||||
}
|
||||
std::string scoring_text(std::uint64_t fingerprint) {
|
||||
static constexpr char digits[] = "0123456789abcdef";
|
||||
@@ -261,24 +354,23 @@ std::optional<std::uint64_t> parse_scoring(std::string_view text) {
|
||||
if (!value) return std::nullopt;
|
||||
return value;
|
||||
}
|
||||
bool may_join(const ServerConfig &config, std::uint64_t id, std::size_t on) noexcept {
|
||||
bool may_join(const ServerConfig &config, std::uint64_t id, std::size_t on, std::size_t reserved_on) noexcept {
|
||||
if (on >= config.max_players) return false;
|
||||
const auto listed = [&](const std::vector<std::uint64_t> &ids) { return std::find(ids.begin(), ids.end(), id) != ids.end(); };
|
||||
return on + config.reserved_slots < config.max_players || listed(config.reserved) || listed(config.admins);
|
||||
if (listed(config.reserved) || listed(config.admins)) return true;
|
||||
const auto kept = config.reserved.size() - std::min(reserved_on, config.reserved.size());
|
||||
return on + kept < config.max_players;
|
||||
}
|
||||
std::string config_error(const ServerConfig &c) {
|
||||
using namespace multiplayer;
|
||||
if (!valid_server_name(c.name)) return std::string("name must be ") + server_name_rule + ".";
|
||||
for (const auto id : c.reserved)
|
||||
if (!individual_steam_id(id)) return "reserved must be SteamID64s (17 digits starting 7656119).";
|
||||
if (c.reserved_slots >= c.max_players) return "reserved_slots must be less than max_players, so that anyone can join at all.";
|
||||
if (c.reserved.size() > 1024) return "reserved holds at most 1024 players.";
|
||||
if (!individual_steam_id(id)) return "reserved_players_slots must be SteamID64s (17 digits starting 7656119).";
|
||||
if (c.reserved.size() >= c.max_players)
|
||||
return "reserved_players_slots must list fewer players than max_players: a slot is kept for each, so nobody else could join.";
|
||||
if (c.send_rate < 128 || c.send_rate > 16384) return "send_rate must be 128 to 16384 (KB/s for each player).";
|
||||
if (c.bone_scale_limit != 0 && !(c.bone_scale_limit >= 1.f && c.bone_scale_limit <= 8.f))
|
||||
return "bone_scale_limit must be 0 (no limit) or 1 to 8 (1: no resized body parts at all).";
|
||||
if (c.connection != "relay" && c.connection != "direct") return "connection must be \"relay\" or \"direct\".";
|
||||
if (!c.direct_address.empty() && !direct_ipv4(c.direct_address)) return "direct_address must be an address like 203.0.113.7, or empty.";
|
||||
if (c.direct_port && c.direct_port == c.query_port) return "direct_port and query_port must differ.";
|
||||
if (c.pack_ms > 50) return "pack_ms must be 0 (off) to 50.";
|
||||
if (c.finger_distance > 10000) return "finger_distance must be 0 (fingers always sent) to 10000.";
|
||||
if (!valid_crowd_budget(c.crowd_budget))
|
||||
|
||||
+12
-18
@@ -37,29 +37,18 @@ struct ServerConfig {
|
||||
std::vector<std::string> map_pool; // maps for votes and the rotation, in order; empty: every map
|
||||
unsigned map_rotation = 0; // minutes per map before the next pool map (0: off)
|
||||
unsigned max_players = 16; // players; the server itself is not one
|
||||
// Of those, how many are kept for the players in `reserved` and the admins: everyone else
|
||||
// is told the server is full once only these are left. 0: none are kept.
|
||||
unsigned reserved_slots = 0;
|
||||
// The most poses a second one player is sent (crowd_limits); 0: no limit.
|
||||
unsigned crowd_budget = crowd_pose_budget;
|
||||
// The most a mod may resize part of a skater for the other players, as a factor (and its
|
||||
// inverse the least): 1, the default, shows every skater at the game's own proportions;
|
||||
// 0 is no limit.
|
||||
float bone_scale_limit = 1;
|
||||
// How players reach the server: "relay", through Steam's relay network only, or "direct",
|
||||
// straight to `port` (UDP). A direct server still answers through the relays, for a player
|
||||
// the port does not reach, one who has turned direct connections off, or an older game.
|
||||
std::string connection = "relay";
|
||||
// The address players connect straight to, a.b.c.d, when it is not the one Steam lists the
|
||||
// server under (a host that sends from one address and takes connections on another).
|
||||
// Empty: the listed one. And the port, when it is not `port`; 0: `port`.
|
||||
std::string direct_address;
|
||||
// How players reach the server: true, through Steam's relay network only; false, straight
|
||||
// to `port` (UDP). A direct server still answers through the relays, for a player the port
|
||||
// does not reach, one who has turned direct connections off, or an older game.
|
||||
bool use_steam_relay = true;
|
||||
// Logs what Steam's networking says it is doing, to find out why connections fail.
|
||||
bool steam_debug = false;
|
||||
// For testing with few players: poses go through the server even between players whose
|
||||
// games have linked to each other.
|
||||
bool relay_everything = false;
|
||||
std::uint16_t direct_port = 0;
|
||||
// How long a message to a player may wait to share a packet with the next ones, in
|
||||
// milliseconds (0: each goes at once in a packet of its own). Fewer, fuller packets:
|
||||
// less sent for the same updates, and less work sending it.
|
||||
@@ -69,6 +58,7 @@ struct ServerConfig {
|
||||
unsigned finger_distance = 25;
|
||||
// What the server may send each player, in KB/s (128-16384).
|
||||
unsigned send_rate = 900;
|
||||
// The players with a reserved slot: one is kept free for each of them who is not on.
|
||||
std::vector<std::uint64_t> reserved;
|
||||
std::string password; // empty: anyone may join
|
||||
std::string welcome; // sent to each player as they join
|
||||
@@ -125,9 +115,13 @@ struct ServerConfig {
|
||||
// their names go to `added` ("votes.seconds" for a nested one).
|
||||
ServerConfig load_config(const std::filesystem::path &file, std::vector<std::string> *added = nullptr);
|
||||
void save_config(const ServerConfig &config);
|
||||
// Whether a player who is not yet on may join a server with `on` players on it: anyone while
|
||||
// an unreserved slot is free, then only the reserved players and the admins until it is full.
|
||||
bool may_join(const ServerConfig &config, std::uint64_t id, std::size_t on) noexcept;
|
||||
// Where the bans are kept: data/bans.json, beside the config.
|
||||
std::filesystem::path bans_file(const ServerConfig &config);
|
||||
// Whether a player who is not yet on may join a server with `on` players on it, `reserved_on`
|
||||
// of them players with a reserved slot. A slot is kept for each reserved player who is not on:
|
||||
// anyone joins while a slot beyond those is free, then only the reserved players and the admins
|
||||
// until the server is full.
|
||||
bool may_join(const ServerConfig &config, std::uint64_t id, std::size_t on, std::size_t reserved_on) noexcept;
|
||||
// Why `config` cannot run, or empty.
|
||||
std::string config_error(const ServerConfig &config);
|
||||
// A scoring fingerprint as the config and console write it (16 hex digits), and read back
|
||||
|
||||
+14
-20
@@ -149,19 +149,18 @@ bool Host::start(std::string &error) {
|
||||
return false;
|
||||
}
|
||||
direct_port_ = 0;
|
||||
if (config_.connection == "direct") {
|
||||
const auto port = config_.direct_port ? config_.direct_port : config_.port;
|
||||
if (!config_.use_steam_relay) {
|
||||
const auto port = config_.port;
|
||||
if (transport_.listen_direct(port)) {
|
||||
direct_port_ = port;
|
||||
log_("Connection: direct, on UDP port " + std::to_string(port) +
|
||||
(config_.direct_address.empty() ? std::string{} : " at " + config_.direct_address) +
|
||||
". The port must be open to the internet; players it does not reach come through Steam's relays.");
|
||||
} else {
|
||||
log_("Connection: direct was asked for, but Steam could not listen on UDP port " + std::to_string(port) +
|
||||
" (in use, or not allowed here). Players connect through Steam's relays only.");
|
||||
}
|
||||
} else {
|
||||
log_("Connection: through Steam's relays (\"connection\": \"direct\" lets players connect straight to the server).");
|
||||
log_("Connection: through Steam's relays (\"use_steam_relay\": false lets players connect straight to the server).");
|
||||
}
|
||||
id_ = transport_.status().local_id;
|
||||
secret_ = nonce();
|
||||
@@ -412,7 +411,7 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6
|
||||
if (distance > p.crowd.half || (config_.distances.valid() && distance > reach * reach)) continue;
|
||||
}
|
||||
// Frequent gameplay streams skip the server once the receiver confirms a direct route.
|
||||
if (source && !config_.relay_everything && (packet.kind == PacketKind::pose || packet.kind == PacketKind::audio) &&
|
||||
if (source && (packet.kind == PacketKind::pose || packet.kind == PacketKind::audio) &&
|
||||
!needs_relay(p.direct_routes, p.route_reported, source->member, now_))
|
||||
continue;
|
||||
// Sound is sent with everyone else's they hear, once a pass (flush_poses).
|
||||
@@ -1399,7 +1398,9 @@ void Host::tick(std::uint64_t now) {
|
||||
continue;
|
||||
}
|
||||
if (!guest) {
|
||||
if (!individual_steam_id(link.id) || !may_join(config_, link.id, guests_.size())) {
|
||||
const auto reserved_on = static_cast<std::size_t>(std::count_if(
|
||||
config_.reserved.begin(), config_.reserved.end(), [&](std::uint64_t id) { return guests_.contains(id); }));
|
||||
if (!individual_steam_id(link.id) || !may_join(config_, link.id, guests_.size(), reserved_on)) {
|
||||
// Full for them: for everyone, or with only the reserved slots left.
|
||||
transport_.disconnect(link.id, guests_.size() >= config_.max_players || !individual_steam_id(link.id)
|
||||
? "The server is full."
|
||||
@@ -2113,34 +2114,27 @@ std::string Host::command(std::string_view line, std::uint64_t admin) {
|
||||
(config_.world_layer_sync ? "." : ". Turn on layer-sync to apply it to everyone."));
|
||||
}
|
||||
if (name == "reserved") {
|
||||
// reserved | reserved slots <n> | reserved add|remove <player or SteamID64>
|
||||
// reserved | reserved add|remove <player or SteamID64>
|
||||
if (!console) return "Only the server console manages reserved slots.";
|
||||
const auto [sub, who] = split(argument);
|
||||
if (sub.empty()) {
|
||||
std::string text = std::to_string(config_.reserved_slots) + " of " + std::to_string(config_.max_players) +
|
||||
" slots are reserved for " + std::to_string(config_.reserved.size()) + " players and the admins";
|
||||
std::string text = std::to_string(config_.reserved.size()) + " of " + std::to_string(config_.max_players) +
|
||||
" slots are reserved, one for each of these players while they are not on";
|
||||
for (const auto id : config_.reserved) {
|
||||
const auto *guest = find(id);
|
||||
text += "\n " + std::to_string(id) + (guest ? " " + guest_name(*guest) : std::string{});
|
||||
}
|
||||
return text;
|
||||
}
|
||||
if (sub == "slots") {
|
||||
const auto slots = number(who);
|
||||
if (!slots || *slots >= config_.max_players) return "reserved slots <0-" + std::to_string(config_.max_players - 1) + ">";
|
||||
config_.reserved_slots = static_cast<unsigned>(*slots);
|
||||
return changed(*slots ? std::to_string(*slots) + " slots are kept for reserved players and admins."
|
||||
: std::string("No slots are reserved."));
|
||||
}
|
||||
auto *guest = target(who);
|
||||
const auto id = guest ? guest->member.id : number(who).value_or(0);
|
||||
if (!individual_steam_id(id) || (sub != "add" && sub != "remove")) return "reserved | reserved slots <n> | reserved add|remove <player or SteamID64>";
|
||||
if (!individual_steam_id(id) || (sub != "add" && sub != "remove")) return "reserved | reserved add|remove <player or SteamID64>";
|
||||
const bool listed = std::find(config_.reserved.begin(), config_.reserved.end(), id) != config_.reserved.end();
|
||||
if (sub == "add") {
|
||||
if (!listed && config_.reserved.size() >= 1024) return "The reserved list is full.";
|
||||
if (!listed && config_.reserved.size() + 1 >= config_.max_players)
|
||||
return "Every slot but one is reserved already: a slot is kept for each reserved player.";
|
||||
if (!listed) config_.reserved.push_back(id);
|
||||
return changed(std::to_string(id) + " has a reserved slot." +
|
||||
(config_.reserved_slots ? std::string{} : " No slots are reserved yet: reserved slots <n>."));
|
||||
return changed(std::to_string(id) + " has a reserved slot.");
|
||||
}
|
||||
std::erase(config_.reserved, id);
|
||||
return changed(std::to_string(id) + " no longer has a reserved slot.");
|
||||
|
||||
@@ -44,7 +44,6 @@ class Host {
|
||||
std::uint64_t secret() const { return secret_; }
|
||||
// The UDP port players may connect straight to, or 0 (config connection).
|
||||
std::uint16_t direct_port() const { return direct_port_; }
|
||||
const std::string &direct_address() const { return config_.direct_address; }
|
||||
|
||||
enum class VoteKind { map, kick, time };
|
||||
|
||||
|
||||
@@ -127,8 +127,7 @@ std::string server_tags(const Advertisement &a) {
|
||||
for (int shift = 60; shift >= 0; shift -= 4) secret += hex[(a.secret >> shift) & 15];
|
||||
auto tags = "reskate,v" + std::to_string(multiplayer::protocol_version) + ",k" + secret + ",p" +
|
||||
std::to_string(a.players) + ",c" + std::to_string(a.max_players) + (a.password ? ",w1" : ",w0");
|
||||
// "d27015", or "d203.0.113.7:27015" for a server reached at another address than it is listed under.
|
||||
if (a.direct_port) tags += ",d" + (a.direct_address.empty() ? std::string{} : a.direct_address + ":") + std::to_string(a.direct_port);
|
||||
if (a.direct_port) tags += ",d" + std::to_string(a.direct_port);
|
||||
tags += ",m" + tag_text(a.map, 24);
|
||||
tags += ",n" + tag_text(a.name, 127 - tags.size() - 2);
|
||||
return tags;
|
||||
|
||||
@@ -13,7 +13,6 @@ struct Advertisement {
|
||||
bool password{}, listed{true};
|
||||
std::uint64_t secret{};
|
||||
std::uint16_t direct_port{}; // players may connect straight to this UDP port; 0: relays only
|
||||
std::string direct_address; // at this address, when it is not the one Steam lists the server under
|
||||
};
|
||||
std::string server_tags(const Advertisement &);
|
||||
|
||||
|
||||
Reference in new issue
Block a user