mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 19:47:08 +02:00
227 lines
11 KiB
C++
227 lines
11 KiB
C++
#include "server_host.h"
|
|
#include "server_text.h"
|
|
|
|
// Parties on a dedicated server. The server owns them (PartyBook): players ask through the
|
|
// game's Social menu or the ReSkate menus (Packet::party) or in chat (/party), and every
|
|
// roster tells everyone who is in which party.
|
|
namespace dingosdk::server {
|
|
namespace {
|
|
std::string_view refusal(PartyBook::Result result) {
|
|
switch (result) {
|
|
case PartyBook::Result::ok: return {};
|
|
case PartyBook::Result::self: return "That's you.";
|
|
case PartyBook::Result::same_party: return "You're already in a party together.";
|
|
case PartyBook::Result::full: return "That party is full.";
|
|
case PartyBook::Result::not_leader: return "Only the party leader can do that.";
|
|
case PartyBook::Result::not_member: return "You're not in a party with them.";
|
|
case PartyBook::Result::no_invite: return "That invite has expired.";
|
|
case PartyBook::Result::closed: return "That party is invite-only.";
|
|
case PartyBook::Result::no_party: return "They're not in a party.";
|
|
case PartyBook::Result::busy: return "They already have too many party invites.";
|
|
case PartyBook::Result::renewed: return "They already have your party invite; it stays open for another minute.";
|
|
}
|
|
return "That can't be done.";
|
|
}
|
|
} // namespace
|
|
|
|
void Host::send_party(Guest &to, PartyAction action, std::uint64_t player) {
|
|
auto notice = packet(PacketKind::party, now_);
|
|
notice.party_action = action;
|
|
notice.party_player = player;
|
|
send_packet(to, notice, true, false);
|
|
}
|
|
void Host::party_notice(std::uint32_t party, std::string_view text, std::uint64_t except) {
|
|
const auto *details = parties_.party(party);
|
|
if (!details) return;
|
|
for (const auto member : details->members)
|
|
if (member != except)
|
|
if (auto *guest = find(member); guest && guest->handshaken) send_chat(text, guest);
|
|
}
|
|
std::string Host::party_status(std::uint64_t id) const {
|
|
const auto name = [&](std::uint64_t player) {
|
|
const auto found = guests_.find(player);
|
|
return found == guests_.end() ? std::to_string(player) : guest_name(*found->second);
|
|
};
|
|
if (id) {
|
|
const auto *details = parties_.party(parties_.party_of(id));
|
|
if (!details) return "You're not in a party. Invite someone with /party invite <player>.";
|
|
std::string text = "Your party (" + std::to_string(details->members.size()) + "/" +
|
|
std::to_string(parties_.limit()) + (details->open ? ", open" : ", invite-only") + "):";
|
|
for (const auto member : details->members)
|
|
text += (member == details->members.front() ? " " : ", ") + name(member) + (member == details->leader ? " (leader)" : "");
|
|
return text;
|
|
}
|
|
if (parties_.parties().empty()) return "No parties.";
|
|
std::string text;
|
|
for (const auto &[party, details] : parties_.parties()) {
|
|
text += "Party " + std::to_string(party) + (details.open ? " (open):" : ":");
|
|
for (const auto member : details.members)
|
|
text += " " + name(member) + (member == details.leader ? "*" : "");
|
|
text += "\n";
|
|
}
|
|
text.pop_back();
|
|
return text;
|
|
}
|
|
|
|
void Host::party_request(Guest &guest, PartyAction action, std::uint64_t player) {
|
|
if (!config_.parties) return reply(guest, "Parties are off on this server.");
|
|
const auto me = guest.member.id;
|
|
const auto my_name = guest_name(guest);
|
|
Guest *other{};
|
|
if (player) {
|
|
other = find(player);
|
|
if (!other || !other->handshaken) return reply(guest, "That player is not on the server.");
|
|
}
|
|
const auto their_name = other ? guest_name(*other) : std::string{};
|
|
using R = PartyBook::Result;
|
|
auto result = R::ok;
|
|
switch (action) {
|
|
case PartyAction::invite:
|
|
result = parties_.invite(me, player, now_);
|
|
if (result == R::ok) {
|
|
send_party(*other, PartyAction::invited, me);
|
|
reply(guest, "Invited " + their_name + " to your party.");
|
|
log_("[party] " + my_name + " invited " + their_name);
|
|
}
|
|
break;
|
|
case PartyAction::accept:
|
|
case PartyAction::join: {
|
|
const auto before = parties_.party_of(me);
|
|
result = action == PartyAction::accept ? parties_.accept(me, player, now_) : parties_.join(me, player, now_);
|
|
if (result == R::ok) {
|
|
const auto party = parties_.party_of(me);
|
|
if (before && before != party) party_notice(before, my_name + " left the party.");
|
|
party_notice(party, my_name + " joined the party.", me);
|
|
reply(guest, "You joined " + their_name + "'s party.");
|
|
log_("[party] " + my_name + " joined " + their_name + "'s party");
|
|
} else if (result == R::closed) {
|
|
// Ask the leader instead: they can invite.
|
|
const auto *details = parties_.party(parties_.party_of(player));
|
|
if (auto *leader = details ? find(details->leader) : nullptr) {
|
|
send_chat(my_name + " would like to join your party. Invite them from their player card or with /party invite " +
|
|
my_name, leader);
|
|
reply(guest, "That party is invite-only; its leader was asked to invite you.");
|
|
return;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case PartyAction::decline:
|
|
result = parties_.decline(me, player);
|
|
if (result == R::ok) send_chat(my_name + " declined your party invite.", other);
|
|
break;
|
|
case PartyAction::leave: {
|
|
const auto party = parties_.party_of(me);
|
|
result = parties_.leave(me);
|
|
if (result == R::ok) {
|
|
party_notice(party, my_name + " left the party.");
|
|
reply(guest, "You left the party.");
|
|
}
|
|
break;
|
|
}
|
|
case PartyAction::kick: {
|
|
const auto party = parties_.party_of(me);
|
|
result = parties_.kick(me, player);
|
|
if (result == R::ok) {
|
|
send_chat("You were removed from the party.", other);
|
|
party_notice(party, their_name + " was removed from the party.");
|
|
}
|
|
break;
|
|
}
|
|
case PartyAction::promote:
|
|
result = parties_.promote(me, player);
|
|
if (result == R::ok) party_notice(parties_.party_of(me), their_name + " now leads the party.");
|
|
break;
|
|
case PartyAction::open:
|
|
case PartyAction::close: {
|
|
const auto *before = parties_.party(parties_.party_of(me));
|
|
const bool was_open = before && before->open;
|
|
result = parties_.set_open(me, action == PartyAction::open);
|
|
if (result == R::ok && was_open != (action == PartyAction::open))
|
|
party_notice(parties_.party_of(me), action == PartyAction::open ? "The party is open: anyone can join."
|
|
: "The party is invite-only.");
|
|
break;
|
|
}
|
|
case PartyAction::invited:
|
|
case PartyAction::withdrawn: return; // the server's own notices
|
|
}
|
|
if (result != R::ok) reply(guest, refusal(result));
|
|
if (parties_.revision() != party_revision_) roster_dirty_ = true;
|
|
}
|
|
|
|
void Host::party_left(std::uint64_t id, const std::string &name) {
|
|
const auto party = parties_.party_of(id);
|
|
parties_.remove(id);
|
|
if (party) party_notice(party, name + " left the server.");
|
|
}
|
|
|
|
void Host::tick_parties() {
|
|
for (const auto &lapsed : parties_.expire(now_)) {
|
|
if (auto *to = find(lapsed.to)) send_party(*to, PartyAction::withdrawn, lapsed.from);
|
|
if (auto *from = find(lapsed.from); from && from->handshaken)
|
|
if (const auto *to = find(lapsed.to)) send_chat("Your party invite to " + guest_name(*to) + " expired.", from);
|
|
}
|
|
for (const auto &withdrawn : parties_.take_withdrawn())
|
|
if (auto *to = find(withdrawn.to)) send_party(*to, PartyAction::withdrawn, withdrawn.from);
|
|
if (parties_.revision() != party_revision_) {
|
|
party_revision_ = parties_.revision();
|
|
roster_dirty_ = true;
|
|
}
|
|
}
|
|
|
|
void Host::party_command(Guest &guest, std::string_view line) {
|
|
if (!config_.parties) return reply(guest, "Parties are off on this server.");
|
|
const auto [first, rest] = split(line);
|
|
const auto verb = lower(first);
|
|
const auto target = [&]() -> std::uint64_t {
|
|
auto *other = match_player(rest);
|
|
if (!other) reply(guest, rest.empty() ? "Name a player." : "No single player matches \"" + std::string(rest) + "\".");
|
|
return other ? other->member.id : 0;
|
|
};
|
|
// The newest invite this player holds (for /party accept and decline without a name).
|
|
const auto newest_invite = [&]() -> std::uint64_t {
|
|
std::uint64_t from{}, latest{};
|
|
for (const auto &invite : parties_.invites())
|
|
if (invite.to == guest.member.id && invite.expires >= latest) from = invite.from, latest = invite.expires;
|
|
if (!from) reply(guest, "You have no party invites.");
|
|
return from;
|
|
};
|
|
if (verb.empty() || verb == "status" || verb == "list") return reply(guest, party_status(guest.member.id));
|
|
if (verb == "help" || verb == "?")
|
|
return reply(guest, "/party invite|join|kick|promote <player>, /party accept|decline [player], /party leave, "
|
|
"/party open|close, /p <message> talks to your party");
|
|
if (verb == "invite") { if (const auto id = target()) party_request(guest, PartyAction::invite, id); return; }
|
|
if (verb == "join") { if (const auto id = target()) party_request(guest, PartyAction::join, id); return; }
|
|
if (verb == "kick" || verb == "remove") { if (const auto id = target()) party_request(guest, PartyAction::kick, id); return; }
|
|
if (verb == "promote" || verb == "lead" || verb == "leader") {
|
|
if (const auto id = target()) party_request(guest, PartyAction::promote, id);
|
|
return;
|
|
}
|
|
if (verb == "accept" || verb == "decline") {
|
|
const auto id = rest.empty() ? newest_invite() : target();
|
|
if (id) party_request(guest, verb == "accept" ? PartyAction::accept : PartyAction::decline, id);
|
|
return;
|
|
}
|
|
if (verb == "leave") return party_request(guest, PartyAction::leave, 0);
|
|
if (verb == "open" || verb == "close") return party_request(guest, verb == "open" ? PartyAction::open : PartyAction::close, 0);
|
|
if (verb == "say" || verb == "chat") return party_chat(guest, rest);
|
|
reply(guest, "Unknown party command. Type /party help.");
|
|
}
|
|
|
|
void Host::party_chat(Guest &guest, std::string_view text) {
|
|
const auto party = parties_.party_of(guest.member.id);
|
|
if (!party) return reply(guest, "You're not in a party.");
|
|
const auto line = clean_chat_text(text);
|
|
if (line.empty()) return reply(guest, "/p <message>");
|
|
// Relayed as the sender's own chat line, only to the rest of their party.
|
|
auto message = packet(PacketKind::chat, now_);
|
|
message.source = guest.member.id;
|
|
message.epoch = guest.member.epoch;
|
|
message.text = clean_chat_text("[Party] " + line);
|
|
log_("[party chat] " + guest_name(guest) + ": " + line);
|
|
for (const auto member : parties_.party(party)->members)
|
|
if (member != guest.member.id)
|
|
if (auto *to = find(member); to && to->handshaken) send_packet(*to, message, true, false);
|
|
}
|
|
} // namespace dingosdk::server
|