Files
Cinderwild/scripts/net/chat.gd

229 lines
8.6 KiB
GDScript

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)