M2.6: server rules (PvP, raiding, locks and friends, death penalty, difficulty, wolf grace), chat, admin commands, server console
This commit is contained in:
228
scripts/net/chat.gd
Normal file
228
scripts/net/chat.gd
Normal file
@@ -0,0 +1,228 @@
|
||||
extends Node
|
||||
## Autoload "Chat": talking on a server, and its commands
|
||||
## (docs/MULTIPLAYER_PLAN.md §6).
|
||||
##
|
||||
## T opens the chat line. Plain text goes to everyone (global chat). A line
|
||||
## starting with / is a command; the server console takes the same commands
|
||||
## without the /, when started with --console (server/run_server.sh does).
|
||||
##
|
||||
## Anyone: /help /players /rules /friend <name> /unfriend <name> /friends
|
||||
## Admins: /kick <name> /ban <name> /unban <name> /admin <name> /save
|
||||
## /time 08:00 /weather clear|cloudy|rain|storm|snow|blizzard|auto
|
||||
## /set <rule> <value> /say <message> /release-name <name>
|
||||
|
||||
signal message(text: String, kind: String) ## client: show this (kind: chat | server | reply)
|
||||
|
||||
const MAX_LEN := 200
|
||||
const GAP := 0.4 ## seconds between one player's lines
|
||||
const ADMIN_ONLY := ["kick", "ban", "unban", "admin", "save", "time", "weather", "set", "say", "release-name"]
|
||||
|
||||
var _last: Dictionary = {} ## peer -> ticks of their last line
|
||||
var _console: Thread
|
||||
var heard: Array = [] ## client: everything shown (tests read it)
|
||||
|
||||
func _ready() -> void:
|
||||
if Net.parse_args(OS.get_cmdline_user_args()).has("console"):
|
||||
Net.state_changed.connect(func(s):
|
||||
if s == "server" and _console == null:
|
||||
_console = Thread.new()
|
||||
_console.start(_read_console))
|
||||
|
||||
# --- client ------------------------------------------------------------------------
|
||||
|
||||
func say(text: String) -> void:
|
||||
text = text.strip_edges()
|
||||
if text.is_empty() or Net.state != "connected":
|
||||
return
|
||||
_c2s_say.rpc_id(1, text.substr(0, MAX_LEN))
|
||||
|
||||
func _show(text: String, kind: String) -> void:
|
||||
heard.append(text)
|
||||
message.emit(text, kind)
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_line(text: String, kind: String) -> void:
|
||||
_show(text, kind)
|
||||
|
||||
# --- server ------------------------------------------------------------------------
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _c2s_say(text: String) -> void:
|
||||
var peer := multiplayer.get_remote_sender_id()
|
||||
if not Net.is_server() or not Net.players.has(peer):
|
||||
return
|
||||
text = text.strip_edges().substr(0, MAX_LEN).replace("\n", " ")
|
||||
if text.is_empty():
|
||||
return
|
||||
var who: String = Net.players[peer].name
|
||||
if text.begins_with("/"):
|
||||
print("[server] %s: %s" % [who, text])
|
||||
run(text.substr(1), peer)
|
||||
return
|
||||
# Plain chat only: a line every GAP seconds, so nobody can flood it.
|
||||
var now := Time.get_ticks_msec()
|
||||
if now - int(_last.get(peer, -100000)) < GAP * 1000.0:
|
||||
_reply(peer, "(Too fast: that line wasn't sent)")
|
||||
return
|
||||
_last[peer] = now
|
||||
print("[chat] %s: %s" % [who, text])
|
||||
_all("%s: %s" % [who, text], "chat")
|
||||
|
||||
func _all(text: String, kind: String) -> void:
|
||||
if kind == "server":
|
||||
print("[server] ", text)
|
||||
for peer in Net.players:
|
||||
_s2c_line.rpc_id(peer, text, kind)
|
||||
|
||||
## An answer to whoever gave the command (peer 0 = the console).
|
||||
func _reply(peer: int, text: String) -> void:
|
||||
if peer == 0:
|
||||
print(text)
|
||||
else:
|
||||
_s2c_line.rpc_id(peer, text, "reply")
|
||||
|
||||
func _is_admin(peer: int) -> bool:
|
||||
return peer == 0 or (Net.players.has(peer) and Net.config.is_admin(String(Net.players[peer].id)))
|
||||
|
||||
## The player id a name belongs to (online or reserved), or "".
|
||||
func _id_of(n: String) -> String:
|
||||
var peer := Net.peer_by_name(n)
|
||||
if peer != 0:
|
||||
return String(Net.players[peer].id)
|
||||
return Net.config.name_owner(n)
|
||||
|
||||
## Runs a command line (without the /) from player `peer`, or the console (0).
|
||||
func run(line: String, peer: int) -> void:
|
||||
var parts := line.strip_edges().split(" ", false)
|
||||
if parts.is_empty():
|
||||
return
|
||||
var cmd := parts[0].to_lower()
|
||||
var rest := " ".join(parts.slice(1))
|
||||
var arg: String = parts[1] if parts.size() > 1 else ""
|
||||
if cmd in ADMIN_ONLY and not _is_admin(peer):
|
||||
_reply(peer, "Only admins can use /%s" % cmd)
|
||||
return
|
||||
var me: String = String(Net.players[peer].id) if Net.players.has(peer) else ""
|
||||
var by: String = String(Net.players[peer].name) if Net.players.has(peer) else "The server"
|
||||
match cmd:
|
||||
"help":
|
||||
_reply(peer, "Commands: /players /rules /friend <name> /unfriend <name> /friends" +
|
||||
(" · admin: /kick /ban /unban /admin /save /time 08:00 /weather <state|auto> /set <rule> <value> /say /release-name" if _is_admin(peer) else ""))
|
||||
"players":
|
||||
var names: Array[String] = []
|
||||
for p in Net.players.values():
|
||||
names.append(String(p.name) + (" (admin)" if Net.config.is_admin(String(p.id)) else ""))
|
||||
_reply(peer, "%d online: %s" % [names.size(), ", ".join(names)])
|
||||
"rules":
|
||||
var c: RefCounted = Net.config
|
||||
_reply(peer, "PvP %s · raiding %s · locks %s · death: %s · sleep: %s · difficulty %s · wolves after day %d" % [
|
||||
"on" if bool(c.v("pvp")) else "off", "on" if bool(c.v("raiding")) else "off", c.v("locks"),
|
||||
c.v("death_penalty"), c.v("sleep"), c.v("difficulty"), int(c.v("wolf_grace_days"))])
|
||||
"friend", "unfriend":
|
||||
if me.is_empty():
|
||||
_reply(peer, "Only players have friends")
|
||||
return
|
||||
var fid := _id_of(arg)
|
||||
if fid.is_empty() or fid == me:
|
||||
_reply(peer, "No player called '%s' here" % arg)
|
||||
elif cmd == "friend":
|
||||
Net.config.add_friend(me, fid)
|
||||
_reply(peer, "%s can now use your doors, chests and beds" % arg)
|
||||
var fp := Net.peer_by_name(arg)
|
||||
if fp != 0:
|
||||
_reply(fp, "%s added you as a friend: you can use their doors, chests and beds" % by)
|
||||
else:
|
||||
_reply(peer, "%s is no longer a friend" % arg if Net.config.remove_friend(me, fid) else "%s wasn't a friend" % arg)
|
||||
"friends":
|
||||
if me.is_empty():
|
||||
return
|
||||
var names: Array[String] = []
|
||||
for fid in Net.config.friends.get(me, []):
|
||||
names.append(Net.config.name_of(String(fid)))
|
||||
_reply(peer, "Your friends: %s" % (", ".join(names) if not names.is_empty() else "nobody yet (/friend <name>)"))
|
||||
"kick":
|
||||
var kp := Net.peer_by_name(arg)
|
||||
if kp == 0:
|
||||
_reply(peer, "Nobody called '%s' is online" % arg)
|
||||
return
|
||||
Net.kick(kp, "You were kicked by %s" % by)
|
||||
_all("%s was kicked" % arg, "server")
|
||||
"ban":
|
||||
var bid := _id_of(arg)
|
||||
if bid.is_empty():
|
||||
_reply(peer, "No player called '%s'" % arg)
|
||||
return
|
||||
Net.config.ban(bid)
|
||||
var bp := Net.peer_by_name(arg)
|
||||
if bp != 0:
|
||||
Net.kick(bp, "You were banned by %s" % by)
|
||||
_all("%s was banned" % arg, "server")
|
||||
"unban":
|
||||
var uid: String = Net.config.name_owner(arg)
|
||||
_reply(peer, "%s can join again" % arg if not uid.is_empty() and Net.config.unban(uid) else "'%s' isn't banned" % arg)
|
||||
"admin":
|
||||
var aid := _id_of(arg)
|
||||
if aid.is_empty():
|
||||
_reply(peer, "No player called '%s'" % arg)
|
||||
return
|
||||
Net.config.add_admin(aid)
|
||||
_reply(peer, "%s is now an admin" % arg)
|
||||
var ap := Net.peer_by_name(arg)
|
||||
if ap != 0:
|
||||
Net.players[ap].admin = true
|
||||
_reply(ap, "%s made you an admin (/help for the commands)" % by)
|
||||
"save":
|
||||
WorldNet.save()
|
||||
_reply(peer, "Saved")
|
||||
"time":
|
||||
var hm := arg.split(":")
|
||||
if hm.size() < 1 or not hm[0].is_valid_int() or int(hm[0]) > 23 or (hm.size() > 1 and not hm[1].is_valid_int()):
|
||||
_reply(peer, "Use /time HH:MM, e.g. /time 08:00")
|
||||
return
|
||||
var w: Node3D = WorldNet.world
|
||||
w.day_night.time = (int(hm[0]) + (int(hm[1]) if hm.size() > 1 else 0) / 60.0) / 24.0
|
||||
w.day_night.apply(true)
|
||||
w.weather.snap()
|
||||
Net.push_clock()
|
||||
_all("%s set the time to %02d:%02d" % [by, int(hm[0]), int(hm[1]) if hm.size() > 1 else 0], "server")
|
||||
"weather":
|
||||
var states: Array = preload("res://scripts/world/weather.gd").STATES
|
||||
if arg != "auto" and not arg in states:
|
||||
_reply(peer, "Use /weather %s or auto" % "|".join(PackedStringArray(states)))
|
||||
return
|
||||
Net.set_weather("" if arg == "auto" else arg)
|
||||
_all("%s set the weather to %s" % [by, arg], "server")
|
||||
"set":
|
||||
if parts.size() < 3:
|
||||
_reply(peer, "Use /set <rule> <value>, e.g. /set pvp true (/rules shows them)")
|
||||
return
|
||||
var why: String = Net.config.set_rule(parts[1], " ".join(parts.slice(2)))
|
||||
if not why.is_empty():
|
||||
_reply(peer, why)
|
||||
return
|
||||
Net.broadcast_rules()
|
||||
_all("%s set %s to %s" % [by, parts[1], " ".join(parts.slice(2))], "server")
|
||||
"say":
|
||||
_all("[Server] %s" % rest, "server")
|
||||
"release-name":
|
||||
_reply(peer, "'%s' is free for anyone now" % arg if Net.config.release_name(arg) else "Nobody has the name '%s'" % arg)
|
||||
_:
|
||||
_reply(peer, "Unknown command /%s (/help lists them)" % cmd)
|
||||
|
||||
# --- server console ------------------------------------------------------------------
|
||||
|
||||
## Reads commands typed into the server's terminal (a thread: reading blocks).
|
||||
func _read_console() -> void:
|
||||
var empty := 0
|
||||
while empty < 20:
|
||||
var line := OS.read_string_from_stdin(1024).strip_edges()
|
||||
if line.is_empty():
|
||||
empty += 1
|
||||
OS.delay_msec(250)
|
||||
continue
|
||||
empty = 0
|
||||
_console_line.call_deferred(line.trim_prefix("/"))
|
||||
|
||||
func _console_line(line: String) -> void:
|
||||
if Net.is_server():
|
||||
run(line, 0)
|
||||
Reference in New Issue
Block a user