summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorraven <citrons@mondecitronne.com>2026-03-21 00:31:49 -0500
committerraven <citrons@mondecitronne.com>2026-03-21 00:31:49 -0500
commit3f6a61d993d6c50135c3cefe3cf362390c4027d5 (patch)
tree8232e80a845480e01a3769269e1d67156085ecba /server
parent22214f3fea9b2e201a9010ff1bf27bf52aeaf338 (diff)
chat commands
Diffstat (limited to 'server')
-rw-r--r--server/commands.go222
-rw-r--r--server/player.go53
-rw-r--r--server/server.go29
3 files changed, 298 insertions, 6 deletions
diff --git a/server/commands.go b/server/commands.go
index abb4e43..0b56aaf 100644
--- a/server/commands.go
+++ b/server/commands.go
@@ -1 +1,223 @@
package server
+
+import (
+ "sort"
+ "strings"
+)
+
+type CommandSender interface {
+ OnCommandOutput(from *Server, output string)
+ OnCommandError(from *Server, err string)
+}
+type commandCtx struct {
+ server *Server
+ sender CommandSender
+ arg arguments
+ auth authLevel
+}
+type commandHandler func(commandCtx) (err string)
+
+func isCommand(message string) (isCmd bool, text string) {
+ text = message
+ if message[0] == '/' {
+ text = message[1:]
+ // an additional slash escapes the second //not command -> /not command
+ isCmd = len(text) != 0 && text[0] != '/'
+ }
+ return
+}
+
+func executeCommand(
+ s *Server, auth authLevel, from CommandSender, command string) {
+ ctx := commandCtx {
+ server: s,
+ sender: from,
+ arg: parseArgs(command),
+ auth: auth,
+ }
+ cmd, _ := ctx.arg.nextArg()
+ handler := commands[cmd]
+ var err string
+ if commandAuth[cmd] > auth {
+ err = "Permission denied"
+ } else if handler == nil {
+ err = "Unknown command: /" + cmd
+ } else {
+ err = handler(ctx)
+ }
+ if err != "" {
+ from.OnCommandError(s, err)
+ }
+}
+
+func usage(cmd string) string {
+ if help[cmd] != nil {
+ return "Usage: " + help[cmd][0]
+ } else {
+ return "Invalid usage"
+ }
+}
+
+var commands = map[string]commandHandler {
+ "op": func(ctx commandCtx) string {
+ name, ok := ctx.arg.nextArg()
+ if !ok {
+ return usage("op")
+ }
+ if !playerNameRegex.Match([]byte(name)) {
+ return "Unknown player."
+ }
+ ctx.server.changePlayerAuth(name, opAuth, func(ok bool) {
+ if ok {
+ ctx.sender.OnCommandOutput(ctx.server, "Opped.")
+ } else {
+ ctx.sender.OnCommandError(ctx.server, "Unknown player")
+ }
+ })
+ return ""
+ },
+ "deop": func(ctx commandCtx) string {
+ name, ok := ctx.arg.nextArg()
+ if !ok {
+ return usage("op")
+ }
+ ctx.server.changePlayerAuth(name, defaultAuth, func(ok bool) {
+ if ok {
+ ctx.sender.OnCommandOutput(ctx.server, "Deopped.")
+ } else {
+ ctx.sender.OnCommandError(ctx.server, "Unknown player")
+ }
+ })
+ return ""
+ },
+ "stop": func(ctx commandCtx) string {
+ ctx.server.Stop(nil)
+ return ""
+ },
+ "save": func(ctx commandCtx) string {
+ ctx.server.Save(nil)
+ return ""
+ },
+ "help": func(ctx commandCtx) string {
+ cmd, ok := ctx.arg.nextArg()
+ if !ok {
+ var usages []string
+ for cmd, helpMessages := range help {
+ if commandAuth[cmd] <= ctx.auth {
+ usages = append(usages, helpMessages[0])
+ }
+ }
+ sort.Strings(usages)
+ ctx.sender.OnCommandOutput(ctx.server, "Available commands:")
+ for _, usage := range usages {
+ ctx.sender.OnCommandOutput(ctx.server, "&7* &e" + usage)
+ }
+ return ""
+ }
+ if help[cmd] == nil {
+ return "Unknown command: /" + cmd
+ }
+ for n, line := range help[cmd] {
+ if n == 0 {
+ line = "Usage: " + line
+ } else {
+ line = " " + line
+ }
+ ctx.sender.OnCommandOutput(ctx.server, line)
+ }
+ return ""
+ },
+}
+
+var commandAuth = map[string]authLevel {
+ "op": opAuth,
+ "deop": opAuth,
+ "save": opAuth,
+ "stop": opAuth,
+}
+
+var help = map[string][]string {
+ "op": []string {
+ "/op <player>",
+ "Grant operator status",
+ },
+ "deop": []string {
+ "/deop <player>",
+ "Revoke operator status",
+ },
+ "save": []string {
+ "/save",
+ "Save all levels",
+ },
+ "stop": []string {
+ "/stop",
+ "Stop the server",
+ },
+ "help": []string {
+ "/help [command]",
+ },
+}
+
+type arguments struct {
+ rd *strings.Reader
+}
+
+func parseArgs(command string) arguments {
+ return arguments {strings.NewReader(command)}
+}
+
+func (arg *arguments) nextArg() (string, bool) {
+ var out strings.Builder
+ for {
+ b, err := arg.rd.ReadByte()
+ if err != nil {
+ return "", false
+ }
+ if b != ' ' {
+ arg.rd.UnreadByte()
+ break
+ }
+ }
+ var splitChar byte = ' '
+ b, err := arg.rd.ReadByte()
+ if err != nil {
+ return "", false
+ }
+ if b == '"' {
+ splitChar = '"'
+ } else {
+ arg.rd.UnreadByte()
+ }
+ for {
+ b, err = arg.rd.ReadByte()
+ if err != nil {
+ if out.Len() > 0 {
+ break
+ }
+ return "", false
+ }
+ if b == splitChar {
+ break
+ }
+ if b == '\\' {
+ b, err = arg.rd.ReadByte()
+ if err != nil {
+ b = '\\'
+ }
+ }
+ out.WriteByte(b)
+ }
+ return out.String(), true
+}
+
+func (arg *arguments) allArgs() []string {
+ var (a string; ok bool; args []string)
+ for {
+ a, ok = arg.nextArg()
+ if !ok {
+ break
+ }
+ args = append(args, a)
+ }
+ return args
+}
diff --git a/server/player.go b/server/player.go
index bd0d915..4a562a7 100644
--- a/server/player.go
+++ b/server/player.go
@@ -18,17 +18,38 @@ type player struct {
level *level
}
+type authLevel int
+
type playerState struct {
LevelId levelId
Pos entityPos
Facing entityFacing
+ Auth authLevel
}
+const (
+ defaultAuth = iota
+ opAuth
+)
+
var playerNameRegex = regexp.MustCompile("^[.-_a-zA-Z0-9]*$")
+func loadPlayerData(
+ from phony.Actor, name string, loaded func(playerState, bool)) {
+ loadDataFile(from, "player/" + name, loaded)
+}
+
+func savePlayerData(
+ from phony.Actor, name string, state playerState, done func()) {
+ saveDataFile(from, "player/" + name, state)
+ if done != nil {
+ dataManager.Act(nil, done)
+ }
+}
+
func newPlayer(s *Server, cl *client, name string) *player {
pl := &player {client: cl, server: s, name: name}
- loadDataFile(pl, "player/" + name, func(state playerState, ok bool) {
+ loadPlayerData(pl, name, func(state playerState, ok bool) {
if ok {
pl.state = state
pl.ChangeLevel(pl, state.LevelId, state.Pos)
@@ -41,10 +62,7 @@ func newPlayer(s *Server, cl *client, name string) *player {
func (p *player) save(done func()) {
os.Mkdir("world/player", 0777)
- saveDataFile(p, "player/" + p.name, p.state)
- if done != nil {
- dataManager.Act(nil, done)
- }
+ savePlayerData(p, p.name, p.state, done)
}
func (p *player) kick(reason string) {
@@ -79,7 +97,16 @@ func (p *player) handlePacket(packet classic.Packet) {
}
p.level.SetBlock(p, pos, block)
case *classic.Message:
- p.server.OnPlayerMessage(p, p.name, classic.UnpadString(pck.Message))
+ message := classic.UnpadString(pck.Message)
+ isCmd, text := isCommand(message)
+ if text == "" {
+ break
+ }
+ if !isCmd {
+ p.server.OnPlayerMessage(p, p.name, text)
+ } else {
+ p.server.ExecuteCommand(p, p.state.Auth, text)
+ }
}
}
@@ -99,6 +126,12 @@ func (p *player) Kick(from phony.Actor, reason string) {
})
}
+func (p *player) SetAuthLevel(from phony.Actor, auth authLevel) {
+ p.Act(from, func() {
+ p.state.Auth = auth
+ })
+}
+
func (p *player) OnPacket(from phony.Actor, packet classic.Packet) {
p.Act(from, func() {
p.handlePacket(packet)
@@ -149,6 +182,14 @@ func (p *player) OnPlayerMessage(from *Server, name string, message string) {
p.SendMessage(from, fmt.Sprintf("&7<&b%s&7>&f %s", name, message))
}
+func (p *player) OnCommandOutput(from *Server, output string) {
+ p.SendMessage(from, "&e" + output)
+}
+
+func (p *player) OnCommandError(from *Server, err string) {
+ p.SendMessage(from, "&c" + err)
+}
+
func (p *player) OnLevelData(from *level, info levelInfo, data io.ReadCloser) {
p.Act(from, func() {
defer data.Close()
diff --git a/server/server.go b/server/server.go
index 1bea3cf..2740e09 100644
--- a/server/server.go
+++ b/server/server.go
@@ -163,6 +163,13 @@ func (s *Server) SendPings() {
})
}
+func (s *Server) ExecuteCommand(
+ from CommandSender, auth authLevel, command string) {
+ s.Act(nil, func() {
+ executeCommand(s, auth, from, command)
+ })
+}
+
func (s *Server) OnDisconnect(cl *client, username string, pl *player) {
s.Act(cl, func() {
delete(s.clients, cl)
@@ -211,6 +218,28 @@ func (s *Server) newPlayer(cl *client, name string) *player {
return pl
}
+func (s *Server) changePlayerAuth(
+ playerName string, auth authLevel, done func(ok bool)) {
+ pl := s.players[playerName]
+ if pl != nil {
+ pl.SetAuthLevel(s, auth)
+ pl.Act(s, func() {
+ done(true)
+ })
+ return
+ }
+ loadPlayerData(s, playerName, func(state playerState, ok bool) {
+ if !ok {
+ done(false)
+ return
+ }
+ state.Auth = auth
+ savePlayerData(s, playerName, state, func() {
+ done(true)
+ })
+ })
+}
+
func (s *Server) NewPlayer(
from phony.Actor, cl *client, name string, reply func(*player)) {
s.Act(from, func() {