This commit is contained in:
Miroslav Vasilev 2025-06-16 14:59:51 +03:00
commit 2d3ad194d6
13 changed files with 415 additions and 0 deletions

15
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch file",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/src/Server/main.go"
},
]
}

86
src/CommandLib/arg.go Normal file
View file

@ -0,0 +1,86 @@
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
}

View file

@ -0,0 +1,49 @@
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
}

View file

@ -0,0 +1,15 @@
package commandlib
type argValue struct {
value any
}
func CreateArgValue(val any) *argValue {
return &argValue{
value: val,
}
}
func (aVal *argValue) Value() any {
return aVal.value
}

71
src/CommandLib/command.go Normal file
View file

@ -0,0 +1,71 @@
package commandlib
type ArgumentBase interface {
Name() string
ArgType() ArgType
IsOptional() bool
Validate(value any) (valid bool, feedback []error)
}
type ArgumentValue interface {
Value() any
}
type command struct {
name string
altname string
args []ArgumentBase
work func(argValues []ArgumentValue) (err error)
}
func CreateCommand(
name string,
altname string,
work func(argValues []ArgumentValue) (err error),
arguments ...ArgumentBase,
) (cmd *command, err error) {
var onlyAcceptingOptionals = false
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
return
}
if v.IsOptional() {
onlyAcceptingOptionals = true
}
}
cmd = new(command)
cmd.name = name
cmd.altname = altname
cmd.work = work
return
}
func (cmd *command) Name() string {
return cmd.name
}
func (cmd *command) DoWork(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)
}

View file

@ -0,0 +1,10 @@
package commandlib
type Command interface {
Name() string
DoWork(argValues []ArgumentValue) (err error)
}
type commandRegistry struct {
commands []Command
}

19
src/CommandLib/error.go Normal file
View file

@ -0,0 +1,19 @@
package commandlib
import "fmt"
type commandLibError struct {
cmdName string
message string
}
func CreateCommandLibError(cmdName string, msg string, msgArgs ...any) *commandLibError {
return &commandLibError{
cmdName: cmdName,
message: fmt.Sprintf(msg, msgArgs...),
}
}
func (cmdErr *commandLibError) Error() string {
return "Error with command '" + cmdErr.cmdName + "': " + cmdErr.message
}

3
src/CommandLib/go.mod Normal file
View file

@ -0,0 +1,3 @@
module code.haedhutner.dev/mvv/LastMUD/CommandLib
go 1.24.4

View file

@ -0,0 +1,22 @@
// BNF of the command language used to interact with the LastMUD server
name ::= letter ( letter | digit )* ;
message ::= word ( space word )* ;
decimal ::= number "." number ; // 1.0, 2.0, 132.183, etc.
number ::= digit ( digit )* ; // 123, 12, 97401, etc.
word ::= letter+;
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"
| "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" ;
digit ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
space ::= " " ;

View file

@ -0,0 +1,30 @@
package commandlib
import (
"strconv"
"strings"
)
func Tokenize(commandMsg string) []any {
split := strings.Split(commandMsg, " ")
tokens := []any{}
for _, v := range split {
valInt, err := strconv.ParseInt(v, 10, 32)
if err == nil {
tokens = append(tokens, valInt)
}
valFloat, err := strconv.ParseFloat(v, 32)
if err == nil {
tokens = append(tokens, valFloat)
}
tokens = append(tokens, v)
}
return tokens
}

6
src/CoreLib/go.mod Normal file
View file

@ -0,0 +1,6 @@
module code.haedhutner.dev/mvv/LastMUD/CoreLib
require code.haedhutner.dev/mvv/LastMUD/CommandLib v0.0.0
replace code.haedhutner.dev/mvv/LastMUD/CommandLib => ../CommandLib
go 1.24.4

13
src/Server/go.mod Normal file
View file

@ -0,0 +1,13 @@
module code.haedhutner.dev/mvv/LastMUD/Server
require (
code.haedhutner.dev/mvv/LastMUD/CommandLib v0.0.0
code.haedhutner.dev/mvv/LastMUD/CoreLib v0.0.0
)
replace (
code.haedhutner.dev/mvv/LastMUD/CommandLib => ../CommandLib
code.haedhutner.dev/mvv/LastMUD/CoreLib => ../CoreLib
)
go 1.24.4

76
src/Server/main.go Normal file
View file

@ -0,0 +1,76 @@
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
commandlib "code.haedhutner.dev/mvv/LastMUD/CommandLib"
)
type Command interface {
Name() string
}
type argValue struct {
value string
}
func main() {
testcmd, err := commandlib.CreateCommand(
"test",
"t",
func(argValues []commandlib.ArgumentValue) (err error) {
err = nil
return
},
commandlib.CreateStringArg("test", "test message"),
)
ln, err := net.Listen("tcp", ":8000")
if err != nil {
log.Fatal(err)
}
fmt.Println("Listening on port 8000")
conn, err := ln.Accept()
if err != nil {
log.Fatal(err)
}
for {
message, err := bufio.NewReader(conn).ReadString('\n')
response := ""
if err != nil {
log.Fatal(err)
}
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())
}
} else {
fmt.Print("Message Received: ", string(message))
response = strings.ToUpper(message)
}
conn.Write([]byte(response + "\n> "))
}
}