From 82082b128f0e8d8af5f57b9d4be2eff71c333011 Mon Sep 17 00:00:00 2001 From: Miroslav Vasilev Date: Wed, 18 Jun 2025 17:04:06 +0300 Subject: [PATCH] CommandLib more or less finished --- src/CommandLib/arg.go | 86 ---------------------- src/CommandLib/arg_validation.go | 49 ------------- src/CommandLib/arg_value.go | 15 ---- src/CommandLib/command.go | 103 +++++++++++++-------------- src/CommandLib/command_context.go | 30 -------- src/CommandLib/command_definition.go | 4 -- src/CommandLib/command_registry.go | 10 --- src/CommandLib/error.go | 8 +-- src/CommandLib/interface.go | 10 --- src/CommandLib/lastmud.w3c-ebnf | 6 +- src/CommandLib/parameter.go | 25 +++++++ src/CommandLib/registry.go | 71 ++++++++++++++++++ src/CommandLib/tokenizer.go | 25 +++---- src/Server/main.go | 84 ++++++++++------------ 14 files changed, 203 insertions(+), 323 deletions(-) delete mode 100644 src/CommandLib/arg.go delete mode 100644 src/CommandLib/arg_validation.go delete mode 100644 src/CommandLib/arg_value.go delete mode 100644 src/CommandLib/command_context.go delete mode 100644 src/CommandLib/command_definition.go delete mode 100644 src/CommandLib/command_registry.go delete mode 100644 src/CommandLib/interface.go create mode 100644 src/CommandLib/parameter.go create mode 100644 src/CommandLib/registry.go diff --git a/src/CommandLib/arg.go b/src/CommandLib/arg.go deleted file mode 100644 index 8feba73..0000000 --- a/src/CommandLib/arg.go +++ /dev/null @@ -1,86 +0,0 @@ -package commandlib - -type ArgType byte - -const ( - StringArg = iota - IntArg - FloatArg -) - -func (a ArgType) String() string { - switch a { - case StringArg: - return "word" - case IntArg: - return "number" - case FloatArg: - return "decimal number" - default: - return "unknown" - } -} - -type arg struct { - name string - help string - optional bool - - argType ArgType - validators []ArgValidator -} - -func CreateArg( - name string, - help string, - optional bool, - argType ArgType, - validators ...ArgValidator, -) (res *arg, err error) { - res = &arg{ - name: name, - help: help, - argType: argType, - validators: validators, - } - - return -} - -func CreateStringArg(name string, help string, validators ...ArgValidator) (res *arg) { - res = &arg{ - name: name, - help: help, - argType: StringArg, - validators: append([]ArgValidator{StringArgTypeValidator}, validators...), - } - - return -} - -func (arg *arg) ArgType() ArgType { - return arg.argType -} - -func (arg *arg) Name() string { - return arg.name -} - -func (arg *arg) IsOptional() bool { - return arg.optional -} - -func (arg *arg) Validate(value any) (valid bool, feedback []error) { - feedback = []error{} - - for _, validate := range arg.validators { - err := validate(value) - - if err != nil { - valid = false - feedback = append(feedback, err) - } - } - - return -} diff --git a/src/CommandLib/arg_validation.go b/src/CommandLib/arg_validation.go deleted file mode 100644 index 1b54e78..0000000 --- a/src/CommandLib/arg_validation.go +++ /dev/null @@ -1,49 +0,0 @@ -package commandlib - -import "fmt" - -type ArgValidator = func(value any) (err error) - -type argValidationError struct { - message string -} - -func CreateArgValidationError(template string, args ...any) *argValidationError { - return &argValidationError{ - message: fmt.Sprintf(template, args...), - } -} - -func (err *argValidationError) Error() string { - return err.message -} - -func StringArgTypeValidator(value any) (err error) { - _, valid := value.(string) - - if !valid { - err = CreateArgValidationError("Invalid argument type, expected %v", StringArg) - } - - return -} - -func IntArgTypeValidator(value any) (err error) { - _, valid := value.(int32) - - if !valid { - err = CreateArgValidationError("Invalid type, expected %v", IntArg) - } - - return -} - -func FloatArgTypeValidator(value any) (err error) { - _, valid := value.(float32) - - if !valid { - err = CreateArgValidationError("Invalid type, expected %v", FloatArg) - } - - return -} diff --git a/src/CommandLib/arg_value.go b/src/CommandLib/arg_value.go deleted file mode 100644 index 5b3d14f..0000000 --- a/src/CommandLib/arg_value.go +++ /dev/null @@ -1,15 +0,0 @@ -package commandlib - -type argValue struct { - value any -} - -func CreateArgValue(val any) *argValue { - return &argValue{ - value: val, - } -} - -func (aVal *argValue) Value() any { - return aVal.value -} diff --git a/src/CommandLib/command.go b/src/CommandLib/command.go index c241953..b9e96fa 100644 --- a/src/CommandLib/command.go +++ b/src/CommandLib/command.go @@ -1,71 +1,70 @@ package commandlib -type ArgumentBase interface { - Name() string - ArgType() ArgType - IsOptional() bool - Validate(value any) (valid bool, feedback []error) +type Command struct { + commandDefinition CommandDefinition + params []Parameter } -type ArgumentValue interface { - Value() any +func CreateCommand(cmdDef CommandDefinition, parameters []Parameter) Command { + return Command{ + commandDefinition: cmdDef, + params: parameters, + } } -type command struct { - name string - altname string - - args []ArgumentBase - - work func(argValues []ArgumentValue) (err error) +func (cmd Command) Execute() (err error) { + return cmd.commandDefinition.work(cmd.params...) } -func CreateCommand( - name string, - altname string, - work func(argValues []ArgumentValue) (err error), - arguments ...ArgumentBase, -) (cmd *command, err error) { - var onlyAcceptingOptionals = false +type commandContextError struct { + err string +} - for _, v := range arguments { - if !v.IsOptional() && onlyAcceptingOptionals { - // Optional arguments can only be placed after non-optional ones - err = CreateCommandLibError(name, "Cannot define non-optional arguments after optional ones.") - cmd = nil +func createCommandContextError(err string) *commandContextError { + return &commandContextError{ + err: err, + } +} - return - } +func (cce *commandContextError) Error() string { + return cce.err +} - if v.IsOptional() { - onlyAcceptingOptionals = true - } +type CommandContext struct { + commandString string + tokens []Token + + command Command +} + +func CreateCommandContext(commandRegistry *CommandRegistry, commandString string) (ctx *CommandContext, err error) { + tokenizer := CreateTokenizer() + + tokens, tokenizerError := tokenizer.Tokenize(commandString) + + if tokenizerError != nil { + err = tokenizerError + return } - cmd = new(command) + commandDef := commandRegistry.Match(tokens) - cmd.name = name - cmd.altname = altname - cmd.work = work + if commandDef == nil { + err = createCommandContextError("Unknown command") + return + } + + params := commandDef.ParseParameters(tokens) + + ctx = &CommandContext{ + commandString: commandString, + tokens: tokens, + command: CreateCommand(*commandDef, params), + } return } -func (cmd *command) Name() string { - return cmd.name -} - -func (cmd *command) Execute(argValues []ArgumentValue) (err error) { - - for i, v := range cmd.args { - if i > len(argValues)-1 { - if !v.IsOptional() { - return CreateCommandLibError(cmd.name, "Not enough arguments, found %d, expected more", len(argValues)) - } else { - break // There are no more arg values to process, and the remaining arguments are all optional anyway - } - } - } - - return cmd.work(argValues) +func (ctx *CommandContext) ExecuteCommand() (err error) { + return ctx.command.Execute() } diff --git a/src/CommandLib/command_context.go b/src/CommandLib/command_context.go deleted file mode 100644 index 5987254..0000000 --- a/src/CommandLib/command_context.go +++ /dev/null @@ -1,30 +0,0 @@ -package commandlib - -type commandContext struct { - commandString string - tokens []Token - - command Command -} - -func CreateCommandContext(commandString string) (ctx *commandContext, err error) { - tokenizer := CreateTokenizer() - - tokens, tokenizerError := tokenizer.Tokenize(commandString) - - if tokenizerError != nil { - err = tokenizerError - return - } - - ctx = &commandContext{ - commandString: commandString, - tokens: tokens, - } - - return -} - -func (ctx *commandContext) Execute() (err error) { - ctx.command.Execute() -} diff --git a/src/CommandLib/command_definition.go b/src/CommandLib/command_definition.go deleted file mode 100644 index fa0307a..0000000 --- a/src/CommandLib/command_definition.go +++ /dev/null @@ -1,4 +0,0 @@ -package commandlib - -type CommandDefinition struct { -} diff --git a/src/CommandLib/command_registry.go b/src/CommandLib/command_registry.go deleted file mode 100644 index 90d206e..0000000 --- a/src/CommandLib/command_registry.go +++ /dev/null @@ -1,10 +0,0 @@ -package commandlib - -type Command interface { - Name() string - DoWork(argValues []ArgumentValue) (err error) -} - -type commandRegistry struct { - commands []Command -} diff --git a/src/CommandLib/error.go b/src/CommandLib/error.go index 7ae83f4..b52aa94 100644 --- a/src/CommandLib/error.go +++ b/src/CommandLib/error.go @@ -2,18 +2,18 @@ package commandlib import "fmt" -type commandLibError struct { +type commandError struct { cmdName string message string } -func CreateCommandLibError(cmdName string, msg string, msgArgs ...any) *commandLibError { - return &commandLibError{ +func createCommandError(cmdName string, msg string, msgArgs ...any) *commandError { + return &commandError{ cmdName: cmdName, message: fmt.Sprintf(msg, msgArgs...), } } -func (cmdErr *commandLibError) Error() string { +func (cmdErr *commandError) Error() string { return "Error with command '" + cmdErr.cmdName + "': " + cmdErr.message } diff --git a/src/CommandLib/interface.go b/src/CommandLib/interface.go deleted file mode 100644 index f8ad6fe..0000000 --- a/src/CommandLib/interface.go +++ /dev/null @@ -1,10 +0,0 @@ -package commandlib - -type Parameter interface { - Value() any -} - -type Command interface { - Name() string - Parameters() []Parameter -} diff --git a/src/CommandLib/lastmud.w3c-ebnf b/src/CommandLib/lastmud.w3c-ebnf index 9ec64e7..6751350 100644 --- a/src/CommandLib/lastmud.w3c-ebnf +++ b/src/CommandLib/lastmud.w3c-ebnf @@ -45,7 +45,7 @@ look ::= "look" [ "around" | direction | "at" identifier ] ; move ::= "move" direction | "go" direction ; -// [Player Name], [Item Name], [Place Name] or just Played Name, Item Name, Place Name +// [Player Name], [Item Name], [Place Name] or just Player Name, Item Name, Place Name // brackets may be useful in situations where there are multiple identifiers identifier ::= "[" name "]" | name ; @@ -57,12 +57,10 @@ number ::= digit ( digit )* ; // 123, 12, 97401, etc. word ::= letter+; -chatMessage ::= ( letter | punctuation | digit | space )+ ; +chatMessage ::= ( letter | digit | space )+ ; direction ::= "east" | "west" | "north" | "up" | "down" ; -punctuation ::= "," | "." | "!" | "?" | "'" | "/" | '"' | ":" | ";" | "-" | "(" | ")" | "[" | "]" ; - letter ::= "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" ; diff --git a/src/CommandLib/parameter.go b/src/CommandLib/parameter.go new file mode 100644 index 0000000..e3fa447 --- /dev/null +++ b/src/CommandLib/parameter.go @@ -0,0 +1,25 @@ +package commandlib + +import "strconv" + +type Parameter struct { + value string +} + +func CreateParameter(value string) Parameter { + return Parameter{ + value: value, + } +} + +func (p Parameter) AsString() (res string, err error) { + return p.value, nil +} + +func (p Parameter) AsInteger() (res int, err error) { + return strconv.Atoi(p.value) +} + +func (p Parameter) AsDecimal() (res float64, err error) { + return strconv.ParseFloat(p.value, 32) +} diff --git a/src/CommandLib/registry.go b/src/CommandLib/registry.go new file mode 100644 index 0000000..8894fd8 --- /dev/null +++ b/src/CommandLib/registry.go @@ -0,0 +1,71 @@ +package commandlib + +import "log" + +type TokenMatcher func(tokens []Token) bool + +type ParameterParser func(tokens []Token) []Parameter + +type CommandWork func(parameters ...Parameter) (err error) + +type CommandDefinition struct { + name string + tokenMatcher TokenMatcher + parameterParser ParameterParser + work CommandWork +} + +func CreateCommandDefinition( + name string, + tokenMatcher TokenMatcher, + parameterParser ParameterParser, + work CommandWork, +) CommandDefinition { + return CommandDefinition{ + name: name, + tokenMatcher: tokenMatcher, + parameterParser: parameterParser, + work: work, + } +} + +func (def CommandDefinition) Name() string { + return def.name +} + +func (def CommandDefinition) Match(tokens []Token) bool { + return def.tokenMatcher(tokens) +} + +func (def CommandDefinition) ParseParameters(tokens []Token) []Parameter { + return def.parameterParser(tokens) +} + +func (def CommandDefinition) ExecuteFunc() CommandWork { + return def.work +} + +type CommandRegistry struct { + commandDefinitions []CommandDefinition +} + +func CreateCommandRegistry(commandDefinitions ...CommandDefinition) *CommandRegistry { + return &CommandRegistry{ + commandDefinitions: commandDefinitions, + } +} + +func (comReg *CommandRegistry) Register(newCommandDefinitions ...CommandDefinition) { + comReg.commandDefinitions = append(comReg.commandDefinitions, newCommandDefinitions...) +} + +func (comReg *CommandRegistry) Match(tokens []Token) (comDef *CommandDefinition) { + for _, v := range comReg.commandDefinitions { + if v.Match(tokens) { + log.Println("Found match", v.Name()) + return &v + } + } + + return +} diff --git a/src/CommandLib/tokenizer.go b/src/CommandLib/tokenizer.go index 9beaa50..ff0c7bd 100644 --- a/src/CommandLib/tokenizer.go +++ b/src/CommandLib/tokenizer.go @@ -20,7 +20,6 @@ const ( TokenDirection TokenCommand - TokenSayCommand TokenSelf TokenWhitespace @@ -46,8 +45,6 @@ func (tt TokenType) String() string { return "Direction" case TokenCommand: return "Command" - case TokenSayCommand: - return "SayCommand" case TokenSelf: return "Self" case TokenWhitespace: @@ -92,12 +89,15 @@ type tokenPattern struct { pattern string } -type tokenizer struct { +// Used to tokenize a string input. +// This is the starting point for parsing a command string. +// Create with [CreateTokenizer] +type Tokenizer struct { tokenPatterns []tokenPattern } -func CreateTokenizer() *tokenizer { - return &tokenizer{ +func CreateTokenizer() *Tokenizer { + return &Tokenizer{ tokenPatterns: []tokenPattern{ {tokenType: TokenDecimal, pattern: `\b\d+\.\d+\b`}, {tokenType: TokenNumber, pattern: `\b\d+\b`}, @@ -111,15 +111,16 @@ func CreateTokenizer() *tokenizer { } } -func (t *tokenizer) Tokenize(commandMsg string) (tokens []Token, err error) { +// Tokenize a command string +func (t *Tokenizer) Tokenize(commandString string) (tokens []Token, err error) { tokens = []Token{} pos := 0 - inputLen := len(commandMsg) + inputLen := len(commandString) // Continue iterating until we reach the end of the input for pos < inputLen { matched := false - remaining := commandMsg[pos:] + remaining := commandString[pos:] // Iterate through each token type and test its pattern for _, pattern := range t.tokenPatterns { @@ -133,7 +134,7 @@ func (t *tokenizer) Tokenize(commandMsg string) (tokens []Token, err error) { return } - // If the loc isn't nil, that means we've found a match + // If the location of the match isn't nil, that means we've found a match if loc := re.FindStringIndex(remaining); loc != nil { lexeme := remaining[loc[0]:loc[1]] @@ -145,9 +146,9 @@ func (t *tokenizer) Tokenize(commandMsg string) (tokens []Token, err error) { } } - // Unknown tokens are still added, except carriage return (\r) and newline (\n) + // Unknown tokens are still added if !matched { - tokens = append(tokens, CreateToken(TokenUnknown, commandMsg[pos:pos+1], pos)) + tokens = append(tokens, CreateToken(TokenUnknown, commandString[pos:pos+1], pos)) pos++ } } diff --git a/src/Server/main.go b/src/Server/main.go index 25aea94..b346b5f 100644 --- a/src/Server/main.go +++ b/src/Server/main.go @@ -5,9 +5,6 @@ import ( "fmt" "log" "net" - "strings" - - commandlib "code.haedhutner.dev/mvv/LastMUD/CommandLib" ) type Command interface { @@ -15,18 +12,6 @@ type Command interface { } func main() { - // testcmd, err := commandlib.CreateCommand( - // "test", - // "t", - // func(argValues []commandlib.ArgumentValue) (err error) { - // err = nil - // return - // }, - // commandlib.CreateStringArg("test", "test message"), - // ) - - tokenizer := commandlib.CreateTokenizer() - ln, err := net.Listen("tcp", ":8000") if err != nil { @@ -41,47 +26,52 @@ func main() { log.Fatal(err) } + // cmdRegistry := commandlib.CreateCommandRegistry( + // commandlib.CreateCommandDefinition( + // "exit", + // func(tokens []commandlib.Token) bool { + // return tokens[0].Lexeme() == "exit" + // }, + // func(tokens []commandlib.Token) []commandlib.Parameter { + // return nil + // }, + // func(parameters ...commandlib.Parameter) (err error) { + // err = conn.Close() + // return + // }, + // ), + // ) + for { - message, err := bufio.NewReader(conn).ReadString('\n') + message, _ := bufio.NewReader(conn).ReadString('\n') response := "" - if err != nil { - log.Fatal(err) - } + // if err != nil { + + // if err == io.EOF { + // fmt.Println("Client disconnected") + // break + // } + + // log.Println("Read error:", err) + + // continue + // } conn.Write([]byte(message + "\n")) - tokens, err := tokenizer.Tokenize(message) + // cmdContext, err := commandlib.CreateCommandContext(cmdRegistry, message) - if err != nil { - response = err.Error() - } else { - lines := make([]string, len(tokens)) - - for i, tok := range tokens { - lines[i] = tok.String() - } - - response = strings.Join(lines, "\n") - } - - // if strings.HasPrefix(message, testcmd.Name()) { - // tokens := commandlib.Tokenize(message) - // args := []commandlib.ArgumentValue{} - - // for _, v := range tokens[1:] { - // args = append(args, commandlib.CreateArgValue(v)) - // } - - // err := testcmd.DoWork(args) - - // if err != nil { - // fmt.Print(err.Error()) - // } + // if err != nil { + // log.Println(err) + // response = err.Error() // } else { - // fmt.Print("Message Received: ", string(message)) + // // err = cmdContext.ExecuteCommand() - // response = strings.ToUpper(message) + // // if err != nil { + // // log.Println(err) + // // response = err.Error() + // // } // } conn.Write([]byte(response + "\n> "))