mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 15:07:06 +02:00
Merge branch 'main' into claude/peaceful-bardeen-ode7bj
This commit is contained in:
21 files changed
+389
-60
No files matched your search
@@ -35,4 +35,18 @@ inline constexpr std::uintptr_t named_lookup = 0x16fc900;
|
||||
inline constexpr std::array<unsigned char, 24> named_lookup_prefix{
|
||||
0x40,0x55,0x56,0x57,0x41,0x56,0x48,0x81,0xec,0xd8,0x02,0x00,0x00,0x48,0x8b,0x05,
|
||||
0xac,0x7a,0xac,0x05,0x48,0x33,0xc4,0x48};
|
||||
// The skater scripts' item:addEsShaderPreset(material, preset): a Lua function (state) -> count
|
||||
// of results. Its third argument is a reference to the loaded preset, which it uses without
|
||||
// checking that the preset is there: an item whose preset has gone (or never loaded) by the
|
||||
// time its script runs crashes the game in the material's construction.
|
||||
inline constexpr std::uintptr_t add_shader_preset = 0x52cb70;
|
||||
inline constexpr std::array<unsigned char, 16> add_shader_preset_prefix{
|
||||
0x40,0x53,0x55,0x56,0x57,0x41,0x56,0x41,0x57,0x48,0x83,0xec,0x28,0x48,0x8b,0xd9};
|
||||
// The two calls it makes to read that argument: (state, argument number) -> the reference, and
|
||||
// (reference, 0) -> the preset it holds, or null.
|
||||
inline constexpr std::uintptr_t script_preset_reference = 0x336f060;
|
||||
inline constexpr std::array<unsigned char, 8> script_preset_reference_prefix{0x4c,0x8d,0x05,0x81,0xa2,0x0d,0x03,0xe9};
|
||||
inline constexpr std::uintptr_t referenced_preset = 0x16fe9c0;
|
||||
inline constexpr std::array<unsigned char, 17> referenced_preset_prefix{
|
||||
0x48,0x8b,0x09,0x48,0x85,0xc9,0x74,0x07,0x48,0x8b,0x01,0x48,0xff,0x60,0x18,0x33,0xc0};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#include "exit_watch.h"
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
namespace dingosdk::exit_watch {
|
||||
namespace {
|
||||
// How long a close may take once the window is gone or has stopped answering, and how long a
|
||||
// window that still answers is watched (the game may ask before it closes, and be told no).
|
||||
constexpr ULONGLONG grace_ms = 8000, watch_ms = 120000;
|
||||
std::atomic<bool> watching{};
|
||||
std::atomic<ULONGLONG> destroyed_at{};
|
||||
|
||||
void watch(HWND window) noexcept {
|
||||
const auto asked = GetTickCount64();
|
||||
ULONGLONG stuck_since{};
|
||||
for (;;) {
|
||||
Sleep(500);
|
||||
const auto now = GetTickCount64();
|
||||
DWORD_PTR result{};
|
||||
// Gone, or not taking messages: the close is under way, or stuck.
|
||||
const bool answers = !destroyed_at.load() && IsWindow(window) &&
|
||||
SendMessageTimeoutW(window, WM_NULL, 0, 0, SMTO_ABORTIFHUNG, 1000, &result);
|
||||
if (answers) {
|
||||
stuck_since = 0;
|
||||
// Still the game as it was: the close was turned down. Watch the next one afresh.
|
||||
if (now - asked >= watch_ms) { watching = false; return; }
|
||||
continue;
|
||||
}
|
||||
if (!stuck_since) stuck_since = now;
|
||||
if (now - stuck_since < grace_ms) continue;
|
||||
logging::log(logging::Level::warning, logging::Channel::runtime,
|
||||
"The game did not finish closing {} s after it was asked to; ending it.", (now - asked) / 1000);
|
||||
logging::flush();
|
||||
TerminateProcess(GetCurrentProcess(), 0);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void note_window_message(HWND window, UINT message, WPARAM wp) noexcept {
|
||||
const bool closing = message == WM_CLOSE || message == WM_DESTROY || (message == WM_SYSCOMMAND && (wp & 0xfff0) == SC_CLOSE) ||
|
||||
(message == WM_ENDSESSION && wp);
|
||||
if (!closing) return;
|
||||
if (message == WM_DESTROY) destroyed_at = GetTickCount64();
|
||||
if (watching.exchange(true)) return;
|
||||
try {
|
||||
std::thread(watch, window).detach();
|
||||
} catch (...) {
|
||||
watching = false;
|
||||
}
|
||||
}
|
||||
} // namespace dingosdk::exit_watch
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
|
||||
namespace dingosdk::exit_watch {
|
||||
// The game sometimes never finishes closing: its window goes (or stops answering) and the
|
||||
// process stays, holding the game's files and Steam's "running" state. Once the player has
|
||||
// asked the game to close, this gives it a few seconds and then ends the process.
|
||||
// Called from the game window's procedure with each message of that window.
|
||||
void note_window_message(HWND window, UINT message, WPARAM wp) noexcept;
|
||||
}
|
||||
@@ -34,6 +34,43 @@ void **lookup(void **out, const char *name, void *table, std::uint8_t flag, void
|
||||
text.empty() ? "unnamed" : text);
|
||||
return out;
|
||||
}
|
||||
// item:addEsShaderPreset(material, preset) with a preset that is not there: nothing is added,
|
||||
// and the script goes on as it does after any other call.
|
||||
using ScriptFunction = int (*)(void *state);
|
||||
std::atomic<ScriptFunction> original_add_preset{};
|
||||
std::uintptr_t image{};
|
||||
int add_preset(void *state) {
|
||||
const auto reference = reinterpret_cast<void *(*)(void *, int)>(image + native::script_preset_reference)(state, 3);
|
||||
if (reference && reinterpret_cast<void *(*)(void *, int)>(image + native::referenced_preset)(reference, 0))
|
||||
return original_add_preset.load()(state);
|
||||
static std::atomic<unsigned> seen{};
|
||||
const auto count = seen.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
if (count <= 5 || count % 500 == 0)
|
||||
logging::log(logging::Level::warning, logging::Channel::customization,
|
||||
"An item's shader preset was not loaded when its script used it ({} so far); the item is shown without it.",
|
||||
count);
|
||||
return 0;
|
||||
}
|
||||
template<std::size_t Size> bool matches(std::uintptr_t address, const std::array<unsigned char, Size> &prefix) {
|
||||
std::array<unsigned char, Size> bytes{};
|
||||
return memory::read_bytes(address, bytes.data(), bytes.size()) && bytes == prefix;
|
||||
}
|
||||
bool start_add_preset(std::uintptr_t base) {
|
||||
if (!matches(base + native::add_shader_preset, native::add_shader_preset_prefix) ||
|
||||
!matches(base + native::script_preset_reference, native::script_preset_reference_prefix) ||
|
||||
!matches(base + native::referenced_preset, native::referenced_preset_prefix))
|
||||
return false;
|
||||
image = base;
|
||||
auto *target = reinterpret_cast<void *>(base + native::add_shader_preset);
|
||||
void *previous{};
|
||||
if (hook_prepare(target, reinterpret_cast<void *>(&add_preset), &previous) != HookOk) return false;
|
||||
original_add_preset = reinterpret_cast<ScriptFunction>(previous);
|
||||
if (hook_enable(target) != HookOk) {
|
||||
hook_remove(target);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool start(std::uintptr_t base) noexcept {
|
||||
@@ -50,7 +87,7 @@ bool start(std::uintptr_t base) noexcept {
|
||||
hook_remove(target);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return start_add_preset(base);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
namespace dingosdk::preset_lookup_guard {
|
||||
// A preset whose bundle cannot be found (a cosmetic mod's registration missing
|
||||
// from the character bundle-reference table) is treated as not found instead
|
||||
// of crashing the game in the named lookup.
|
||||
// of crashing the game in the named lookup. So is an item's shader preset that is
|
||||
// not loaded when the item's script comes to use it: the item goes without it.
|
||||
bool start(std::uintptr_t base) noexcept;
|
||||
}
|
||||
@@ -352,6 +352,29 @@ void limit_bone_scale(Pose &pose, float limit) noexcept {
|
||||
for (auto &t : pose.skater) hold(t);
|
||||
for (auto &t : pose.board) hold(t);
|
||||
}
|
||||
void limit_bone_reach(Pose &pose, float limit) noexcept {
|
||||
if (!(limit > 0.f)) return;
|
||||
const auto hold = [](std::array<float, 3> &position, const std::array<float, 3> &from, float most) {
|
||||
float squared{};
|
||||
for (unsigned i = 0; i < 3; ++i) squared += (position[i] - from[i]) * (position[i] - from[i]);
|
||||
if (!(squared > most * most)) return;
|
||||
const float factor = most / std::sqrt(squared);
|
||||
for (unsigned i = 0; i < 3; ++i) position[i] = from[i] + (position[i] - from[i]) * factor;
|
||||
};
|
||||
constexpr std::array<float, 3> parent{};
|
||||
// Bone 1 is the rig's place in the world (offset_pose): it goes with the root.
|
||||
for (std::size_t i = 0; i < pose.skater.size(); ++i) {
|
||||
if (i == 1) continue;
|
||||
const bool free = std::find(free_skater_bones.begin(), free_skater_bones.end(), i) != free_skater_bones.end();
|
||||
hold(pose.skater[i].position, parent, free ? std::max(limit, free_bone_reach) : limit);
|
||||
}
|
||||
// The board is its own actor (0) with its rig's place in the world (2): wherever it was
|
||||
// left, but in one piece.
|
||||
for (std::size_t i = 1; i < pose.board.size(); ++i) {
|
||||
if (i == 2) hold(pose.board[i].position, pose.board[0].position, std::max(limit, 5.f));
|
||||
else hold(pose.board[i].position, parent, limit);
|
||||
}
|
||||
}
|
||||
void coarsen_rotations(Pose &pose, unsigned bits) noexcept {
|
||||
bits = std::min(bits, 12U);
|
||||
coarsen(pose.root, std::min(bits, 6U)); // which way they face matters from further off than a finger does
|
||||
|
||||
@@ -301,6 +301,18 @@ void coarsen_rotations(Pose &pose, unsigned bits) noexcept;
|
||||
// that resizes part of a skater (a head four times the size) does it with a bone's scale,
|
||||
// which travels in the pose and so shows to everyone, mod or not.
|
||||
void limit_bone_scale(Pose &pose, float limit) noexcept;
|
||||
// Keeps every bone of the body and the board within `limit` metres of the bone it hangs from
|
||||
// (0: no limit). A bone's place is relative to its parent and, on the game's rigs, never
|
||||
// changes: the longest is a thigh, 0.44 m. A hacked client that moves them stretches its
|
||||
// skater across the map for everyone. The skater's few bones that follow the board and the
|
||||
// body's place in a fall (free_skater_bones) do move, by metres, and are held to
|
||||
// free_bone_reach instead; so is the board's own rig from the board.
|
||||
inline constexpr std::array<std::uint16_t, 14> free_skater_bones{3, 50, 283, 375, 376, 377, 378, 379, 380, 390, 391, 392, 393, 394};
|
||||
inline constexpr float free_bone_reach = 100.f;
|
||||
// What a game holds every pose it is sent to, whatever sent it: twice the server's standard
|
||||
// reach, and the most a server may allow a bone to be resized.
|
||||
inline constexpr float client_bone_reach = 2.f, client_bone_scale = 8.f;
|
||||
void limit_bone_reach(Pose &pose, float limit) noexcept;
|
||||
// The same, with a pose encoded at another update interval (a recipient thinned by
|
||||
// distance) instead of the packet's own, so the packet need not be copied for it.
|
||||
std::vector<std::uint8_t> encode(const Packet &, bool compact_pose, std::uint32_t pose_interval_us);
|
||||
|
||||
@@ -169,6 +169,10 @@ template <class P> bool PoseBuffer::push_frame(P &&p, std::uint64_t arrival) {
|
||||
}
|
||||
const auto source = sender_clock_ ? p.time_us : arrival;
|
||||
frames_.push_back({std::forward<P>(p).pose, arrival, source});
|
||||
// Whatever sent it (a server that checks nothing, or a player's own game): a skater is
|
||||
// never shown stretched across the map or blown up past what any server allows.
|
||||
limit_bone_reach(frames_.back().pose, client_bone_reach);
|
||||
limit_bone_scale(frames_.back().pose, client_bone_scale);
|
||||
interpolation_delay_us_ = std::max(100000U, p.pose_interval_us + 50000);
|
||||
while (frames_.size() > 64)
|
||||
frames_.pop_front();
|
||||
|
||||
@@ -173,6 +173,34 @@ void coarse_checks() {
|
||||
for (std::size_t i = 0; i < std::min(first.size(), second.size()); ++i) different += first[i] != second[i];
|
||||
check(first.size() == second.size() && different < first.size() / 20, "A turn far below the step still changed the pose's bytes");
|
||||
}
|
||||
// A bone moved away from its parent is brought back to the limit, the way it pointed; the
|
||||
// bones that follow the board keep their metres, and no limit leaves everything.
|
||||
{
|
||||
Pose stretched;
|
||||
stretched.skater.resize(395);
|
||||
stretched.board.resize(18);
|
||||
stretched.root.position = stretched.skater[1].position = {500, 20, -300};
|
||||
stretched.skater[7].position = {0, .4f, 0};
|
||||
stretched.skater[20].position = {0, 30, 40};
|
||||
stretched.skater[50].position = {0, 9, 0};
|
||||
stretched.skater[393].position = {0, 0, 4000};
|
||||
stretched.board[0].position = {530, 20, -300};
|
||||
stretched.board[2].position = {530, 20, 700};
|
||||
stretched.board[5].position = {12, 0, 0};
|
||||
auto reached = stretched, loose = stretched;
|
||||
limit_bone_reach(reached, 1.f);
|
||||
limit_bone_reach(loose, 0.f);
|
||||
const auto near = [](const std::array<float, 3> &a, const std::array<float, 3> &b) {
|
||||
return std::abs(a[0] - b[0]) < 1e-3f && std::abs(a[1] - b[1]) < 1e-3f && std::abs(a[2] - b[2]) < 1e-3f;
|
||||
};
|
||||
check(reached.skater[7] == stretched.skater[7] && reached.skater[1] == stretched.skater[1] && reached.root == stretched.root &&
|
||||
reached.skater[50] == stretched.skater[50] && reached.board[0] == stretched.board[0],
|
||||
"The reach limit moved a bone that was where the game puts it");
|
||||
check(near(reached.skater[20].position, {0, .6f, .8f}) && near(reached.skater[393].position, {0, 0, 100}) &&
|
||||
near(reached.board[2].position, {530, 20, -295}) && near(reached.board[5].position, {1, 0, 0}),
|
||||
"A stretched bone was not brought back to the reach limit");
|
||||
check(loose.skater == stretched.skater && loose.board == stretched.board, "No reach limit still moved a bone");
|
||||
}
|
||||
// A resized bone is held to the limit both ways, and at 1 is not resized at all.
|
||||
auto big = fixture();
|
||||
big.pose.skater[5].scale = {4, 4, 4};
|
||||
|
||||
@@ -356,13 +356,49 @@ int bench(const char *path) {
|
||||
return 2;
|
||||
}
|
||||
std::vector<QuantPose> poses;
|
||||
// How far each bone is ever put from where it is measured from: the figures the reach
|
||||
// limit (limit_bone_reach) has to leave room for.
|
||||
std::vector<float> skater_reach, board_reach;
|
||||
float anchor_reach{}, board_from_root{}, board_anchor_reach{};
|
||||
const auto length_of = [](const std::array<float, 3> &v) { return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); };
|
||||
const auto between = [&](const std::array<float, 3> &a, const std::array<float, 3> &b) {
|
||||
return length_of({a[0] - b[0], a[1] - b[1], a[2] - b[2]});
|
||||
};
|
||||
for (;;) {
|
||||
std::uint64_t time{};
|
||||
std::uint32_t length{};
|
||||
if (!in.read(reinterpret_cast<char *>(&time), 8) || !in.read(reinterpret_cast<char *>(&length), 4) || length > 1 << 20) break;
|
||||
std::vector<std::uint8_t> raw(length);
|
||||
if (!in.read(reinterpret_cast<char *>(raw.data()), length)) break;
|
||||
if (const auto packet = dingosdk::multiplayer::decode(raw); packet && packet->kind == PacketKind::pose) poses.push_back(quantize(packet->pose));
|
||||
// A recording made by an earlier version: poses are laid out as they were.
|
||||
if (raw.size() > 6) raw[4] = static_cast<std::uint8_t>(dingosdk::multiplayer::protocol_version), raw[5] = static_cast<std::uint8_t>(dingosdk::multiplayer::protocol_version >> 8);
|
||||
if (const auto packet = dingosdk::multiplayer::decode(raw); packet && packet->kind == PacketKind::pose) {
|
||||
poses.push_back(quantize(packet->pose));
|
||||
const auto &pose = packet->pose;
|
||||
skater_reach.resize(std::max(skater_reach.size(), pose.skater.size()));
|
||||
board_reach.resize(std::max(board_reach.size(), pose.board.size()));
|
||||
for (std::size_t i = 0; i < pose.skater.size(); ++i)
|
||||
if (i != 1) skater_reach[i] = std::max(skater_reach[i], length_of(pose.skater[i].position));
|
||||
if (pose.skater.size() > 1) anchor_reach = std::max(anchor_reach, between(pose.skater[1].position, pose.root.position));
|
||||
for (std::size_t i = 0; i < pose.board.size(); ++i)
|
||||
if (i != 0 && i != 2) board_reach[i] = std::max(board_reach[i], length_of(pose.board[i].position));
|
||||
if (!pose.board.empty()) board_from_root = std::max(board_from_root, between(pose.board[0].position, pose.root.position));
|
||||
if (pose.board.size() > 2) board_anchor_reach = std::max(board_anchor_reach, between(pose.board[2].position, pose.board[0].position));
|
||||
}
|
||||
}
|
||||
{
|
||||
const auto furthest = [](const std::vector<float> &reach, const char *what) {
|
||||
std::vector<std::size_t> order(reach.size());
|
||||
for (std::size_t i = 0; i < order.size(); ++i) order[i] = i;
|
||||
std::sort(order.begin(), order.end(), [&](auto a, auto b) { return reach[a] > reach[b]; });
|
||||
std::cout << what << " bones furthest from their parent:";
|
||||
for (std::size_t i = 0; i < std::min<std::size_t>(order.size(), 16); ++i) std::cout << ' ' << order[i] << '=' << reach[order[i]] << "m";
|
||||
std::cout << '\n';
|
||||
};
|
||||
furthest(skater_reach, "skater");
|
||||
furthest(board_reach, "board");
|
||||
std::cout << "skater anchor from root " << anchor_reach << " m; board from root " << board_from_root << " m; board anchor from board "
|
||||
<< board_anchor_reach << " m\n";
|
||||
}
|
||||
if (poses.size() < 100) {
|
||||
std::cerr << "Too few poses in " << path << '\n';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "Engine/Core/Platform/launcher_support.h"
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
#include "Extension/Boot/exit_watch.h"
|
||||
#include "overlay_internal.h"
|
||||
#include "input_capture.h"
|
||||
#include "playstation_input.h"
|
||||
@@ -120,6 +121,8 @@ LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||
if (window != s.window.load())
|
||||
return previous ? CallWindowProcW(previous, window, message, wp, lp)
|
||||
: DefWindowProcW(window, message, wp, lp);
|
||||
// The player closing the game: it is ended if it does not finish by itself.
|
||||
dingosdk::exit_watch::note_window_message(window, message, wp);
|
||||
struct ReleaseOnOpen {
|
||||
HWND window; WNDPROC previous; bool was_visible;
|
||||
~ReleaseOnOpen() {
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#include "Extension/Assets/native_render_resource_pool.h"
|
||||
#include "Extension/Rendering/display_startup.h"
|
||||
#include "Extension/World/native_route_lookahead.h"
|
||||
#include "Extension/World/unload_guard.h"
|
||||
#include "Extension/World/native_entity_pages.h"
|
||||
#include "Extension/World/physics_world_size.h"
|
||||
#include <string>
|
||||
@@ -58,9 +57,10 @@ bool initialize_bootstrap(std::uintptr_t base) {
|
||||
if (!stage(Channel::world, "Physics world pools (65000 static bodies)", ready, error)) return false;
|
||||
error.clear(); ready = start_native_route_lookahead(base, error);
|
||||
if (!stage(Channel::world, "NPC route cycle and endpoint guards", ready, error)) return false;
|
||||
// A guard against one of the game's own crashes: without it the game runs as shipped.
|
||||
error.clear(); ready = start_unload_guard(base, error);
|
||||
(void)stage(Channel::world, "Level unload crash guard", ready, error);
|
||||
// The level unload guard (unload_guard.h) is not installed: stepping over an entry with no
|
||||
// asset stopped the crash, but that entry had been registered when the level loaded, and
|
||||
// left registered it kept the level from ever finishing its unload (a map change that never
|
||||
// ends, which is worse than the crash). It goes back in once it can release what it skips.
|
||||
if (!stage(Channel::graphics, "Display startup settings", start_display_settings(base), {})) return false;
|
||||
error.clear(); ready = start_user_data_redirect(error);
|
||||
if (!stage(Channel::runtime, "Separate game user data", ready, error)) return false;
|
||||
|
||||
+26
-1
@@ -177,6 +177,14 @@ bone_scale_limit How far a mod may resize part of a skater for the other
|
||||
leaves height alone and still halves the largest heads.
|
||||
The player with the mod still sees it on their own screen.
|
||||
Console: bone-scale <1-8>|off.
|
||||
bone_reach_limit How far, in metres, a bone of a skater's body or board may
|
||||
be from the one it hangs from for the other players: 0.5
|
||||
to 20 (default 1); 0 is no limit. The game never moves
|
||||
them (the longest, a thigh, is 0.44), so the default
|
||||
changes nothing for an ordinary player and stops a hacked
|
||||
game from stretching its skater across the map. Every
|
||||
player's own game also holds what it is shown to 2.
|
||||
Console: bone-reach <0.5-20>|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.
|
||||
@@ -272,6 +280,17 @@ polls Who may ask everyone a question with up to six answers:
|
||||
Players answer on the card or with /1, /2... A poll runs
|
||||
nothing; it ends after "poll_seconds" (default 60), or when
|
||||
whoever started it (or an admin) types /poll end.
|
||||
Server votes The console (and scripts that talk to it) starts the same
|
||||
votes and polls: "vote tod night", "vote map grom",
|
||||
"vote noclip off", "poll Next map? | Grom | Stadium" and
|
||||
"poll end". A vote still has to be on, and its "min_players"
|
||||
met; a poll may be asked whatever "polls" says. The server has
|
||||
no vote of its own and no cooldown. Chat reads "The server
|
||||
started a vote to ..." or "The server asks: ...".
|
||||
"poll-run <command> | <question> | <answer>..." also runs a
|
||||
command for the answer that wins, {answer} replaced by it
|
||||
(nothing on a tie or when nobody answered), e.g.
|
||||
poll-run tod {answer} | Time of day? | morning | noon | night
|
||||
|
||||
"announcements" - Messages from the server.
|
||||
messages Lines the server posts in turn, one every "interval_minutes"
|
||||
@@ -360,7 +379,13 @@ and change voice, distances, placement and kicks from the Multiplayer menu.
|
||||
is map, kick, tod or a custom vote's name, which
|
||||
also takes on|off|<percent>.
|
||||
votes polls off|admins|everyone votes poll-seconds <n> votes starter-yes on|off
|
||||
announce <text> Tell everyone, on a card.
|
||||
vote <map|kick|tod|<custom vote>> [argument] Start a vote as the server
|
||||
(see Server votes).
|
||||
poll <question> | <answer> | <answer>... Ask everyone (2 to 6 answers).
|
||||
poll end End the running poll now.
|
||||
poll-run <command with {answer}> | <question> | <answer>... A poll whose
|
||||
winning answer runs the command (console only).
|
||||
announce <text> Tell everyone, in chat and on a card.
|
||||
announce-to <player> <text> Tell one player, on a card.
|
||||
announcements [list | add <text> | remove <n> | clear | interval <minutes>|off]
|
||||
The messages posted on a timer.
|
||||
|
||||
@@ -77,6 +77,10 @@ int run() {
|
||||
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("anti_cheat.bone_reach_limit") && config.bone_reach_limit == 1, "bone_reach_limit is not a new setting of 1");
|
||||
ServerConfig reached;
|
||||
reached.bone_reach_limit = 0.1f;
|
||||
check(config_error(reached).find("bone_reach_limit") != std::string::npos, "A bone reach limit under half a metre was accepted");
|
||||
check(has("network.pack_ms") && config.pack_ms == 10, "pack_ms is not a new setting of 10");
|
||||
check(has("network.threads") && config.threads == 0, "threads is not a new setting of 0");
|
||||
check(has("network.finger_distance") && config.finger_distance == 25, "finger_distance is not a new setting of 25");
|
||||
|
||||
@@ -15,10 +15,12 @@ namespace {
|
||||
constexpr std::string_view help_text =
|
||||
"status | net [player] | players | say <text> | msg <player> <text> | msg-party <player> <text> | msg-admins <text> | kick <player> | ban <player or SteamID64> [name] | unban <SteamID64> | bans\n"
|
||||
"map <name, e.g. San Vansterdam> | maps | name <text> | password <text|off> | welcome <text|off> | listed on|off\n"
|
||||
"voice on|off | voice-range <50-1000> | distances <full> <half> <half-return> <low> | crowd <n>|off | rate <KB/s> | bone-scale <1-8>|off\n"
|
||||
"voice on|off | voice-range <50-1000> | distances <full> <half> <half-return> <low> | crowd <n>|off | rate <KB/s> | bone-scale <1-8>|off | bone-reach <0.5-20>|off\n"
|
||||
"placement everyone|admins|nobody | objects <number>|off | object-scaling on|off | effects on|off | clear-objects | noclip on|off | nobail on|off | boosts on|off | tuning on|off\n"
|
||||
"tpall [player] | tphere <player> | votes [<vote> on|off|<percent>|seconds|cooldown|min-players <n>] | vote-cancel\n"
|
||||
"votes polls off|admins|everyone | votes poll-seconds <n> | votes starter-yes on|off\n"
|
||||
"vote <map|kick|tod|<custom vote>> [argument] | poll <question> | <answer> | <answer>... | poll end\n"
|
||||
"poll-run <command with {answer}> | <question> | <answer> | <answer>...\n"
|
||||
"announce <text> | announce-to <player> <text> | announcements [list|add <text>|remove <n>|clear|interval <minutes>|off]\n"
|
||||
"map-pool [add|remove <map>|clear] | rotation [<minutes>|off]\n"
|
||||
"park <lot> <layout> | park random | layer-sync on|off | layer <key> default|on|off | tod <time|default>\n"
|
||||
@@ -354,6 +356,20 @@ std::string Host::command(std::string_view line, std::uint64_t admin) {
|
||||
: value == 1.f ? std::string("Skaters show at the game's own proportions: resized body parts are not passed on.")
|
||||
: "Resized body parts show at up to " + std::to_string(value).substr(0, 4) + "x.");
|
||||
}
|
||||
if (name == "bone-reach") {
|
||||
const bool off = argument == "off";
|
||||
float value{};
|
||||
const auto parsed = std::from_chars(argument.data(), argument.data() + argument.size(), value);
|
||||
if (!off && (parsed.ec != std::errc{} || parsed.ptr != argument.data() + argument.size() || !(value >= .5f && value <= 20.f)))
|
||||
return "bone-reach <0.5-20>|off: how many metres a bone of a skater may be from the one it hangs from (now " +
|
||||
(config_.bone_reach_limit > 0.f ? std::to_string(config_.bone_reach_limit).substr(0, 4) : std::string("off")) + ")";
|
||||
config_.bone_reach_limit = off ? 0.f : value;
|
||||
// Whole states go again so that nobody keeps a reference with the old places in it.
|
||||
for (auto &[id, guest] : guests_)
|
||||
for (const auto &[other, unused] : guests_) guest->sender.forget(other, PacketKind::pose);
|
||||
return changed(off ? std::string("Skaters' bones may be moved any distance.")
|
||||
: "Skaters' bones show at most " + std::to_string(value).substr(0, 4) + " m from where they hang.");
|
||||
}
|
||||
if (name == "crowd") {
|
||||
// crowd <poses a second>|off: the most one player is sent (crowd_limits).
|
||||
auto value = number(argument);
|
||||
@@ -494,6 +510,40 @@ std::string Host::command(std::string_view line, std::uint64_t admin) {
|
||||
cancel_vote(console ? "the server cancelled it" : "an admin cancelled it");
|
||||
return "Vote cancelled.";
|
||||
}
|
||||
if (name == "vote") {
|
||||
// A vote started by the server: the same card and checks as /vote, without a cooldown or a vote of its own.
|
||||
Guest *by = console ? nullptr : find(admin);
|
||||
const auto [what_text, rest] = split(argument);
|
||||
const auto what = lower(what_text);
|
||||
const auto started = [](std::string why) { return why.empty() ? std::string("Vote started.") : why; };
|
||||
if (what.empty()) return vote_ ? running_vote_text() : std::string("vote <map|kick|tod|<custom vote>> [argument]");
|
||||
if (what == "map") return started(start_vote(by, VoteKind::map, rest));
|
||||
if (what == "kick") return started(start_vote(by, VoteKind::kick, rest));
|
||||
if (what == "tod" || what == "time") return started(start_vote(by, VoteKind::time, rest));
|
||||
for (std::size_t i = 0; i < config_.votes.custom.size(); ++i)
|
||||
if (config_.votes.custom[i].name == what) return started(start_vote(by, VoteKind::custom, rest, i));
|
||||
return "No vote is called \"" + what + "\": vote map, kick, tod or a custom vote's name (votes lists them).";
|
||||
}
|
||||
if (name == "poll") {
|
||||
Guest *by = console ? nullptr : find(admin);
|
||||
if (lower(trim(argument)) == "end") {
|
||||
const auto why = end_poll(by);
|
||||
return why.empty() ? "Poll ended." : why;
|
||||
}
|
||||
const auto why = start_poll(by, argument);
|
||||
return why.empty() ? "Poll started." : why;
|
||||
}
|
||||
if (name == "poll-run") {
|
||||
// "poll-run <command with {answer}> | <question> | <answer>...": the winner's command runs as
|
||||
// the console's, so only the console may set one up.
|
||||
if (!console) return "poll-run is for the server console only.";
|
||||
const auto bar = argument.find('|');
|
||||
const auto run = trim(argument.substr(0, bar == std::string_view::npos ? 0 : bar));
|
||||
if (run.empty())
|
||||
return "poll-run <command with {answer}> | <question> | <answer> | <answer>..., e.g. poll-run tod {answer} | Time of day? | morning | night";
|
||||
const auto why = start_poll(nullptr, argument.substr(bar + 1), std::string(run));
|
||||
return why.empty() ? "Poll started; the winning answer runs: " + std::string(run) : why;
|
||||
}
|
||||
if (name == "tpall" || name == "tphere") {
|
||||
// Where they go: the admin who asked, or (tpall from the console) the named player.
|
||||
Guest *to{};
|
||||
|
||||
@@ -116,6 +116,7 @@ Layout layout(const ServerConfig &c) {
|
||||
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);
|
||||
anti_cheat.set("bone_reach_limit", c.bone_reach_limit);
|
||||
|
||||
auto &network = root.section("network");
|
||||
network.set("use_steam_relay", c.use_steam_relay);
|
||||
@@ -317,6 +318,7 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector<std::str
|
||||
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.bone_reach_limit = get("anti_cheat", "bone_reach_limit", c.bone_reach_limit);
|
||||
|
||||
c.use_steam_relay = get("network", "use_steam_relay", c.use_steam_relay);
|
||||
c.send_rate = get("network", "send_rate", c.send_rate);
|
||||
@@ -573,6 +575,8 @@ std::string config_error(const ServerConfig &c) {
|
||||
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.bone_reach_limit != 0 && !(c.bone_reach_limit >= .5f && c.bone_reach_limit <= 20.f))
|
||||
return "bone_reach_limit must be 0 (no limit) or 0.5 to 20 metres.";
|
||||
if (c.pack_ms > 50) return "pack_ms must be 0 (off) to 50.";
|
||||
if (c.threads > 32) return "threads must be 0 (one for each processor but one) to 32.";
|
||||
if (c.finger_distance > 10000) return "finger_distance must be 0 (fingers always sent) to 10000.";
|
||||
|
||||
@@ -79,6 +79,9 @@ struct ServerConfig {
|
||||
// inverse the least). The game's own skater height is a scale as well, so 1 shows every
|
||||
// skater at one height and build; 2, the default, leaves height alone. 0 is no limit.
|
||||
float bone_scale_limit = 2;
|
||||
// How far (metres) a bone of a skater's body or board may be from the one it hangs from
|
||||
// for the other players (limit_bone_reach). 0: no limit.
|
||||
float bone_reach_limit = 1;
|
||||
// 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.
|
||||
|
||||
@@ -284,9 +284,10 @@ Host::Guest::KeptPose *Host::keep_pose(Guest &from, const Packet &packet) {
|
||||
while (kept.size() >= 96) kept.pop_front();
|
||||
if (!kept.empty() && kept.back().sequence == packet.sequence) return &kept.back();
|
||||
// What a mod resized on its player's skater reaches the others only as far as the server allows.
|
||||
if (config_.bone_scale_limit >= 1.f) {
|
||||
if (config_.bone_scale_limit >= 1.f || config_.bone_reach_limit > 0.f) {
|
||||
auto pose = packet.pose;
|
||||
limit_bone_scale(pose, config_.bone_scale_limit);
|
||||
limit_bone_reach(pose, config_.bone_reach_limit);
|
||||
kept.push_back({packet.sequence, packet.time_us, now_, 0, pose_codec::quantize(pose)});
|
||||
} else {
|
||||
kept.push_back({packet.sequence, packet.time_us, now_, 0, pose_codec::quantize(packet.pose)});
|
||||
@@ -536,6 +537,7 @@ void Host::broadcast(const Packet &packet, bool reliable, bool fresh, std::uint6
|
||||
if (packet.kind == PacketKind::pose) {
|
||||
// What a mod resized on its player's skater reaches the others only as far as the server allows.
|
||||
if (config_.bone_scale_limit >= 1.f) limit_bone_scale(data.packet.pose, config_.bone_scale_limit);
|
||||
limit_bone_reach(data.packet.pose, config_.bone_reach_limit);
|
||||
}
|
||||
data.raw = encode(data.packet, true);
|
||||
data.wire = encode_wire_bytes(data.raw);
|
||||
|
||||
@@ -191,7 +191,7 @@ class Host {
|
||||
Log log_;
|
||||
ActivityLog activity_; // what players do, for the console (config_.activity_log)
|
||||
// The running player vote (server_votes.cpp), and when each player may start another.
|
||||
// A poll is one too: it has answers instead of yes and no, and runs nothing.
|
||||
// A poll is one too: it has answers instead of yes and no, and runs nothing (but the console's poll-run).
|
||||
struct Vote {
|
||||
VoteKind kind{};
|
||||
std::size_t custom{}; // custom: which of config_.votes.custom
|
||||
@@ -201,6 +201,7 @@ class Host {
|
||||
std::set<std::uint64_t> yes, no;
|
||||
std::vector<std::string> answers; // poll
|
||||
std::map<std::uint64_t, std::size_t> chosen; // poll: each voter's answer
|
||||
std::string run; // poll-run: the console command for the winner ("{answer}")
|
||||
std::uint64_t ends{};
|
||||
};
|
||||
std::optional<Vote> vote_;
|
||||
@@ -334,11 +335,12 @@ class Host {
|
||||
// Chat commands and votes (server_votes.cpp).
|
||||
void chat_command(Guest &, std::string_view line);
|
||||
void run_custom_command(Guest &, const CustomCommand &, std::string_view argument);
|
||||
void start_vote(Guest &, VoteKind, std::string_view argument, std::size_t custom = 0);
|
||||
void start_poll(Guest &, std::string_view text);
|
||||
// Starting a vote or poll: nullptr is the server console; the text says why it did not start ("" = it did).
|
||||
std::string start_vote(Guest *starter, VoteKind, std::string_view argument, std::size_t custom = 0);
|
||||
std::string start_poll(Guest *starter, std::string_view text, std::string run = {});
|
||||
void cast_vote(Guest &, bool yes);
|
||||
void answer_poll(Guest &, std::size_t answer);
|
||||
void end_poll(Guest &);
|
||||
std::string end_poll(Guest *by);
|
||||
void check_vote(bool expired);
|
||||
void cancel_vote(const std::string &why);
|
||||
const VoteSetting &vote_setting(VoteKind, std::size_t custom = 0) const;
|
||||
|
||||
+64
-46
@@ -167,9 +167,12 @@ void Host::chat_command(Guest &guest, std::string_view line) {
|
||||
return static_cast<std::size_t>(word[0] - '1');
|
||||
};
|
||||
if (const auto chosen = answer(verb)) return answer_poll(guest, *chosen);
|
||||
// Starting a vote or poll answers only when it did not start.
|
||||
const auto say = [&](const std::string &why) {
|
||||
if (!why.empty()) reply(guest, why);
|
||||
};
|
||||
if (verb == "poll") {
|
||||
if (lower(trim(rest)) == "end") return end_poll(guest);
|
||||
return start_poll(guest, rest);
|
||||
return say(lower(trim(rest)) == "end" ? end_poll(&guest) : start_poll(&guest, rest));
|
||||
}
|
||||
if (verb == "yes" || verb == "y") return cast_vote(guest, true);
|
||||
if (verb == "no" || verb == "n") return cast_vote(guest, false);
|
||||
@@ -179,9 +182,9 @@ void Host::chat_command(Guest &guest, std::string_view line) {
|
||||
if (what == "yes" || what == "y") return cast_vote(guest, true);
|
||||
if (what == "no" || what == "n") return cast_vote(guest, false);
|
||||
if (const auto chosen = answer(what)) return answer_poll(guest, *chosen);
|
||||
if (what == "map") return start_vote(guest, VoteKind::map, argument);
|
||||
if (what == "kick") return start_vote(guest, VoteKind::kick, argument);
|
||||
if (what == "tod" || what == "time") return start_vote(guest, VoteKind::time, argument);
|
||||
if (what == "map") return say(start_vote(&guest, VoteKind::map, argument));
|
||||
if (what == "kick") return say(start_vote(&guest, VoteKind::kick, argument));
|
||||
if (what == "tod" || what == "time") return say(start_vote(&guest, VoteKind::time, argument));
|
||||
if (what == "list") {
|
||||
if (custom.empty()) return reply(guest, "This server has no votes of its own.");
|
||||
std::string text;
|
||||
@@ -191,7 +194,7 @@ void Host::chat_command(Guest &guest, std::string_view line) {
|
||||
return reply(guest, text, static_cast<unsigned>(multiplayer::server_custom_vote_limit));
|
||||
}
|
||||
for (std::size_t i = 0; i < config_.votes.custom.size(); ++i)
|
||||
if (config_.votes.custom[i].name == what) return start_vote(guest, VoteKind::custom, argument, i);
|
||||
if (config_.votes.custom[i].name == what) return say(start_vote(&guest, VoteKind::custom, argument, i));
|
||||
if (vote_) return reply(guest, running_vote_text());
|
||||
return reply(guest, enabled_votes() || !custom.empty() ? "Start one with /vote map, /vote kick, /vote tod or /vote list (see /help)."
|
||||
: "This server has no player votes.");
|
||||
@@ -231,40 +234,43 @@ std::string Host::running_vote_text() const {
|
||||
return "A vote is running: " + vote_->label + ". Type /yes or /no.";
|
||||
}
|
||||
|
||||
void Host::start_vote(Guest &guest, VoteKind kind, std::string_view argument, std::size_t custom) {
|
||||
// `starter`: the player who asked, or nullptr for the server console. Returns why it did not
|
||||
// start, or "" when it did. The server has no vote of its own and no cooldown.
|
||||
std::string Host::start_vote(Guest *starter, VoteKind kind, std::string_view argument, std::size_t custom) {
|
||||
const auto &setting = vote_setting(kind, custom);
|
||||
const std::string slash = starter ? "/" : ""; // usage: "/vote ..." in chat, "vote ..." on the console
|
||||
const bool on = kind == VoteKind::custom ? setting.enabled : (enabled_votes() & vote_bit(kind)) != 0;
|
||||
if (!on)
|
||||
return reply(guest, kind == VoteKind::time && setting.enabled ? std::string("Time of day votes need world layer sync on the server.")
|
||||
: kind == VoteKind::custom ? "That vote is off on this server."
|
||||
: std::string(vote_name(kind)) + " votes are off on this server.");
|
||||
if (vote_) return reply(guest, running_vote_text());
|
||||
if (const auto wait = vote_cooldowns_.find(guest.member.id); wait != vote_cooldowns_.end() && now_ < wait->second)
|
||||
return reply(guest, "Wait " + std::to_string((wait->second - now_) / 1000000 + 1) + " s before starting another vote.");
|
||||
return kind == VoteKind::time && setting.enabled ? std::string("Time of day votes need world layer sync on the server.")
|
||||
: kind == VoteKind::custom ? "That vote is off on this server."
|
||||
: std::string(vote_name(kind)) + " votes are off on this server.";
|
||||
if (vote_) return running_vote_text();
|
||||
if (const auto wait = starter ? vote_cooldowns_.find(starter->member.id) : vote_cooldowns_.end(); wait != vote_cooldowns_.end() && now_ < wait->second)
|
||||
return "Wait " + std::to_string((wait->second - now_) / 1000000 + 1) + " s before starting another vote.";
|
||||
if (players() < setting.min_players)
|
||||
return reply(guest, "That vote needs " + std::to_string(setting.min_players) + " players on (" + std::to_string(players()) + " now).");
|
||||
return "That vote needs " + std::to_string(setting.min_players) + " players on (" + std::to_string(players()) + " now).";
|
||||
Vote vote;
|
||||
vote.kind = kind;
|
||||
vote.custom = custom;
|
||||
vote.starter = guest.member.id;
|
||||
vote.starter = starter ? starter->member.id : 0;
|
||||
switch (kind) {
|
||||
case VoteKind::map: {
|
||||
if (argument.empty()) return reply(guest, "/vote map <map>, e.g. /vote map grom");
|
||||
if (argument.empty()) return slash + "vote map <map>, e.g. " + slash + "vote map grom";
|
||||
// Players vote between the server's own maps (/maps); a raw level path is admins only.
|
||||
const auto *level = find_level(argument);
|
||||
if (!level || !valid_map_destination(map_destination(argument)))
|
||||
return reply(guest, "No single map is called \"" + std::string(argument) + "\".");
|
||||
if (!in_map_pool(config_, level->asset)) return reply(guest, level->name + " is not one of this server's maps.\n" + pool_text());
|
||||
if (map_hash(map_destination(argument)) == map_) return reply(guest, "The server is already on that map.");
|
||||
return "No single map is called \"" + std::string(argument) + "\".";
|
||||
if (!in_map_pool(config_, level->asset)) return level->name + " is not one of this server's maps.\n" + pool_text();
|
||||
if (map_hash(map_destination(argument)) == map_) return "The server is already on that map.";
|
||||
vote.value = std::string(argument);
|
||||
vote.label = "change the map to " + map_label(argument);
|
||||
break;
|
||||
}
|
||||
case VoteKind::kick: {
|
||||
auto *target = match_player(argument);
|
||||
if (!target) return reply(guest, "No single connected player matches \"" + std::string(argument) + "\".");
|
||||
if (target == &guest) return reply(guest, "You cannot vote to kick yourself.");
|
||||
if (is_admin(target->member.id)) return reply(guest, "Admins cannot be kicked by a vote.");
|
||||
if (!target) return "No single connected player matches \"" + std::string(argument) + "\".";
|
||||
if (target == starter) return "You cannot vote to kick yourself.";
|
||||
if (is_admin(target->member.id)) return "Admins cannot be kicked by a vote.";
|
||||
vote.target = target->member.id;
|
||||
vote.label = "kick " + guest_name(*target);
|
||||
break;
|
||||
@@ -272,7 +278,7 @@ void Host::start_vote(Guest &guest, VoteKind kind, std::string_view argument, st
|
||||
case VoteKind::time: {
|
||||
const auto wanted = lower(argument);
|
||||
if (std::find(times.begin(), times.end(), wanted) == times.end())
|
||||
return reply(guest, "/vote tod <default|morning|noon|afternoon|evening|night|weatherday|weathernight>");
|
||||
return slash + "vote tod <default|morning|noon|afternoon|evening|night|weatherday|weathernight>";
|
||||
vote.value = wanted;
|
||||
vote.label = "set the time of day to " + wanted;
|
||||
break;
|
||||
@@ -283,7 +289,7 @@ void Host::start_vote(Guest &guest, VoteKind kind, std::string_view argument, st
|
||||
auto choice = lower(trim(argument));
|
||||
if (v.choices.empty()) choice.clear();
|
||||
else if (std::find(v.choices.begin(), v.choices.end(), choice) == v.choices.end())
|
||||
return reply(guest, "/vote " + v.name + " <" + joined(v.choices, "|") + ">" + (v.description.empty() ? "" : ": " + v.description));
|
||||
return slash + "vote " + v.name + " <" + joined(v.choices, "|") + ">" + (v.description.empty() ? "" : ": " + v.description);
|
||||
vote.value = fill(fill(v.command, "{map}", config_.map), "{arg}", choice);
|
||||
vote.label = v.description.empty() ? v.name + (choice.empty() ? "" : " " + choice)
|
||||
: v.description + (choice.empty() ? "" : ": " + choice);
|
||||
@@ -291,31 +297,36 @@ void Host::start_vote(Guest &guest, VoteKind kind, std::string_view argument, st
|
||||
if (!vote.label.empty() && vote.label.front() >= 'A' && vote.label.front() <= 'Z') vote.label.front() = static_cast<char>(vote.label.front() + 32);
|
||||
break;
|
||||
}
|
||||
case VoteKind::poll: return start_poll(guest, argument);
|
||||
case VoteKind::poll: return start_poll(starter, argument);
|
||||
}
|
||||
if (config_.votes.starter_votes_yes) vote.yes.insert(guest.member.id);
|
||||
if (starter && config_.votes.starter_votes_yes) vote.yes.insert(starter->member.id);
|
||||
if (!++vote_ids_) ++vote_ids_;
|
||||
vote.id = vote_ids_;
|
||||
const auto seconds = setting.seconds ? setting.seconds : config_.votes.seconds;
|
||||
vote.ends = now_ + microseconds(seconds);
|
||||
vote_cooldowns_[guest.member.id] = now_ + microseconds(setting.cooldown ? setting.cooldown : config_.votes.cooldown);
|
||||
if (starter) vote_cooldowns_[starter->member.id] = now_ + microseconds(setting.cooldown ? setting.cooldown : config_.votes.cooldown);
|
||||
const auto label = vote.label;
|
||||
vote_ = std::move(vote);
|
||||
send_chat(guest_name(guest) + " started a vote to " + label + " (" + std::to_string(setting.percent) + "% needed, " +
|
||||
const auto who = starter ? guest_name(*starter) : std::string("The server");
|
||||
send_chat(who + " started a vote to " + label + " (" + std::to_string(setting.percent) + "% needed, " +
|
||||
std::to_string(seconds) + " s). Vote on the card at the right of your screen, or type /yes or /no.");
|
||||
log_("[vote] " + guest_name(guest) + " started a vote to " + label + ".");
|
||||
log_("[vote] " + who + " started a vote to " + label + ".");
|
||||
check_vote(false);
|
||||
return {};
|
||||
}
|
||||
|
||||
void Host::start_poll(Guest &guest, std::string_view text) {
|
||||
// `starter` nullptr: the server console, which may always ask (the "polls" setting is about
|
||||
// players). `run`: a console command run for a clear winner, "{answer}" replaced by it.
|
||||
std::string Host::start_poll(Guest *starter, std::string_view text, std::string run) {
|
||||
using multiplayer::ServerPolls;
|
||||
const auto polls = enabled_polls();
|
||||
const bool admin = is_admin(guest.member.id);
|
||||
if (polls == ServerPolls::off) return reply(guest, "Polls are off on this server.");
|
||||
if (polls == ServerPolls::admins && !admin) return reply(guest, "Only admins can start a poll on this server.");
|
||||
if (vote_) return reply(guest, running_vote_text());
|
||||
if (const auto wait = vote_cooldowns_.find(guest.member.id); !admin && wait != vote_cooldowns_.end() && now_ < wait->second)
|
||||
return reply(guest, "Wait " + std::to_string((wait->second - now_) / 1000000 + 1) + " s before starting a poll.");
|
||||
const std::string slash = starter ? "/" : "";
|
||||
const bool admin = !starter || is_admin(starter->member.id);
|
||||
if (starter && polls == ServerPolls::off) return "Polls are off on this server.";
|
||||
if (polls == ServerPolls::admins && !admin) return "Only admins can start a poll on this server.";
|
||||
if (vote_) return running_vote_text();
|
||||
if (const auto wait = starter ? vote_cooldowns_.find(starter->member.id) : vote_cooldowns_.end(); !admin && wait != vote_cooldowns_.end() && now_ < wait->second)
|
||||
return "Wait " + std::to_string((wait->second - now_) / 1000000 + 1) + " s before starting a poll.";
|
||||
// "<question> | <answer> | <answer>..."
|
||||
std::vector<std::string> parts;
|
||||
for (std::size_t at = 0; at <= text.size();) {
|
||||
@@ -327,25 +338,28 @@ void Host::start_poll(Guest &guest, std::string_view text) {
|
||||
at = bar + 1;
|
||||
}
|
||||
if (parts.size() < 3 || parts.size() > multiplayer::max_vote_answers + 1)
|
||||
return reply(guest, "/poll <question> | <answer> | <answer>... (2 to " + std::to_string(multiplayer::max_vote_answers) +
|
||||
" answers), e.g. /poll Next map? | Grom | San Vansterdam");
|
||||
return slash + "poll <question> | <answer> | <answer>... (2 to " + std::to_string(multiplayer::max_vote_answers) +
|
||||
" answers), e.g. " + slash + "poll Next map? | Grom | San Vansterdam";
|
||||
Vote poll;
|
||||
poll.kind = VoteKind::poll;
|
||||
poll.starter = guest.member.id;
|
||||
poll.starter = starter ? starter->member.id : 0;
|
||||
poll.label = parts.front();
|
||||
poll.answers.assign(parts.begin() + 1, parts.end());
|
||||
poll.run = std::move(run);
|
||||
if (!++vote_ids_) ++vote_ids_;
|
||||
poll.id = vote_ids_;
|
||||
poll.ends = now_ + microseconds(config_.votes.poll_seconds);
|
||||
if (!admin) vote_cooldowns_[guest.member.id] = now_ + microseconds(config_.votes.cooldown);
|
||||
if (!admin) vote_cooldowns_[starter->member.id] = now_ + microseconds(config_.votes.cooldown);
|
||||
std::string choices;
|
||||
for (std::size_t i = 0; i < poll.answers.size(); ++i) choices += (i ? " " : "") + std::string("/") + std::to_string(i + 1) + " " + poll.answers[i];
|
||||
vote_ = std::move(poll);
|
||||
send_chat(guest_name(guest) + " asks: " + vote_->label + " (" + std::to_string(config_.votes.poll_seconds) +
|
||||
const auto who = starter ? guest_name(*starter) : std::string("The server");
|
||||
send_chat(who + " asks: " + vote_->label + " (" + std::to_string(config_.votes.poll_seconds) +
|
||||
" s). Answer on the card at the right of your screen, or type:");
|
||||
send_chat(choices);
|
||||
log_("[poll] " + guest_name(guest) + " started a poll: " + vote_->label + " " + joined(vote_->answers, " | "));
|
||||
log_("[poll] " + who + " started a poll: " + vote_->label + " " + joined(vote_->answers, " | "));
|
||||
check_vote(false);
|
||||
return {};
|
||||
}
|
||||
|
||||
void Host::cast_vote(Guest &guest, bool yes) {
|
||||
@@ -372,11 +386,13 @@ void Host::answer_poll(Guest &guest, std::size_t answer) {
|
||||
check_vote(false);
|
||||
}
|
||||
|
||||
void Host::end_poll(Guest &guest) {
|
||||
if (!vote_ || vote_->kind != VoteKind::poll) return reply(guest, "No poll is running.");
|
||||
if (vote_->starter != guest.member.id && !is_admin(guest.member.id))
|
||||
return reply(guest, "Only whoever started the poll, or an admin, can end it early.");
|
||||
// `by` nullptr: the server console, which may always end it.
|
||||
std::string Host::end_poll(Guest *by) {
|
||||
if (!vote_ || vote_->kind != VoteKind::poll) return "No poll is running.";
|
||||
if (by && vote_->starter != by->member.id && !is_admin(by->member.id))
|
||||
return "Only whoever started the poll, or an admin, can end it early.";
|
||||
check_vote(true);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::set<std::uint64_t> Host::vote_voters(const Vote &vote) const {
|
||||
@@ -455,6 +471,8 @@ void Host::check_vote(bool expired) {
|
||||
const auto line = "Poll \"" + done.label + "\" ended: " + answer_count(done.answers, count) + " (" + outcome + ").";
|
||||
send_chat(line);
|
||||
log_("[poll] " + line);
|
||||
// A console poll-run: the winning answer's command, as the console. A tie or no answer runs nothing.
|
||||
if (!done.run.empty() && top && leaders.size() == 1) log_(command(fill(done.run, "{answer}", leaders.front())));
|
||||
return;
|
||||
}
|
||||
if (vote.kind == VoteKind::kick && !find(vote.target)) return cancel_vote("the player left");
|
||||
|
||||
@@ -97,6 +97,7 @@ add_library(dingosdk_runtime SHARED
|
||||
Extension/World/level_loading.cpp
|
||||
Extension/World/loading_screen.cpp
|
||||
Extension/Customization/preset_lookup_guard.cpp
|
||||
Extension/Boot/exit_watch.cpp
|
||||
Extension/Customization/developer_hoodie.cpp
|
||||
Extension/Customization/developer_board.cpp
|
||||
Extension/Skater/skater_model.cpp
|
||||
|
||||
Reference in new issue
Block a user