From 3f6a61d993d6c50135c3cefe3cf362390c4027d5 Mon Sep 17 00:00:00 2001 From: raven Date: Sat, 21 Mar 2026 00:31:49 -0500 Subject: chat commands --- server/commands.go | 222 +++++++++++++++++++++++++++++++++++++++++++++++++++++ server/player.go | 53 +++++++++++-- server/server.go | 29 +++++++ 3 files changed, 298 insertions(+), 6 deletions(-) (limited to 'server') 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 ", + "Grant operator status", + }, + "deop": []string { + "/deop ", + "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() { -- cgit v1.2.3