Skip to content

Commit cd220f7

Browse files
authored
Interactive config editor (#396)
# Description This provides a basic in-game config editor using the command `server config` and selecting configurations until you find what you want to edit. ## Changes - `server config` initiates the interactive prompts - configs re-validate when changes are made now - better flattening of config files into dot-syntax. - Fixed "locked" fields, they were in an old format. - Updated helpfile. Converted helpfile to `.md` format. ## Notes - Does not accept partial entry to match names - may be worth adding - Does not accept numeric menu selections - may be worth adding ## Example ``` [☾ 10:03PM HP:60/60 MP:32/32]:server config 1. FilePaths - ... 2. GamePlay - ... 3. Integrations - ... 4. LootGoblin - ... 5. Memory - ... 6. Modules - ... 7. Network - ... 8. Roles - ... 9. Scripting - ... 10. Server - ... 11. SpecialRooms - ... 12. TextFormats - ... 13. Timing - ... 14. Translation - ... 15. Validation - ... .: Choose a config option, or "quit": gameplay [gameplay] 1. AllowItemBuffRemoval - false 2. ConsistentAttackMessages - true 3. ContainerSizeMax - 10 4. Death - ... 5. LivesMax - 3 6. LivesOnLevelUp - 1 7. LivesStart - 3 8. MaxAltCharacters - 3 9. MobConverseChance - 3 10. PVP - limited 11. PVPMinimumLevel - 15 12. PricePerLife - 1000000 13. ShopRestockRate - 6 hours 14. XPScale - 100 .: Choose a config option, or "quit": death [gameplay.death] 1. AlwaysDropBackpack - false 2. CorpseDecayTime - 1 hour 3. CorpsesEnabled - true 4. EquipmentDropChance - 0.25 5. PermaDeath - false 6. ProtectionLevels - 5 7. XPPenalty - none .: Choose a config option, or "quit": permadeath .: New value for GamePlay.Death.PermaDeath [false] true GamePlay.Death.PermaDeath has been set to: true ```
1 parent ae6e076 commit cd220f7

File tree

7 files changed

+287
-31
lines changed

7 files changed

+287
-31
lines changed

_datafiles/config.yaml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,11 @@ Server:
7777
# It is a good idea to lock configs related to folder/file paths to prevent
7878
# accidental changes that could break the game.
7979
Locked:
80-
- DataFiles
81-
- PublicHtml
82-
- AdminHtml
83-
- NextRoomId
84-
- Seed
85-
- OnLoginCommands
86-
- BannedNames
80+
- FilePaths
81+
- Server.NextRoomId
82+
- Server.Seed
83+
- Server.OnLoginCommands
84+
- Server.BannedNames
8785

8886
################################################################################
8987
#
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Help for ~server~ (admin command)
2+
3+
The ~server~ command can be used in the following ways:
4+
5+
## Usage:
6+
7+
~server reload-ansi~
8+
Reloads aliases from the ansi alias file
9+
10+
~server stats~
11+
Get stats on the server
12+
13+
~server set~
14+
Lists all server configuration settings
15+
16+
~server set [name] [value]~
17+
Sets a configuration settings (and saves it)
18+
19+
~server config~
20+
Initiates an interactive configuration editor

_datafiles/world/default/templates/admincommands/help/command.server.template

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Help for ~server~ (admin command)
2+
3+
The ~server~ command can be used in the following ways:
4+
5+
## Usage:
6+
7+
~server reload-ansi~
8+
Reloads aliases from the ansi alias file
9+
10+
~server stats~
11+
Get stats on the server
12+
13+
~server set~
14+
Lists all server configuration settings
15+
16+
~server set [name] [value]~
17+
Sets a configuration settings (and saves it)
18+
19+
~server config~
20+
Initiates an interactive configuration editor

_datafiles/world/empty/templates/admincommands/help/command.server.template

Lines changed: 0 additions & 11 deletions
This file was deleted.

internal/configs/configs.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func (c *Config) DotPaths() map[string]any {
114114

115115
func (c *Config) buildDotPaths(v reflect.Value, prefix string, result map[string]any) {
116116
// If the value is a pointer, dereference it.
117-
if v.Kind() == reflect.Ptr {
117+
if v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr {
118118
if v.IsNil() {
119119
return
120120
}
@@ -317,7 +317,12 @@ func SetVal(propertyPath string, newVal string) error {
317317
return err
318318
}
319319

320-
return configData.OverlayOverrides(overrides)
320+
if err = configData.OverlayOverrides(overrides); err != nil {
321+
return err
322+
}
323+
324+
configData.Validate()
325+
return nil
321326
}
322327

323328
func GetConfig() Config {

internal/usercommands/admin.server.go

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package usercommands
22

33
import (
4+
"errors"
45
"fmt"
56
"slices"
67
"sort"
@@ -18,6 +19,11 @@ import (
1819

1920
var (
2021
memoryReportCache = map[string]util.MemoryResult{}
22+
errValueLocked = errors.New("This config value is locked. You must edit the config file directly.")
23+
)
24+
25+
const (
26+
newValuePrompt = `New value for <ansi fg="6">%s</ansi>`
2127
)
2228

2329
/*
@@ -33,6 +39,10 @@ func Server(rest string, user *users.UserRecord, room *rooms.Room, flags events.
3339
}
3440

3541
args := util.SplitButRespectQuotes(rest)
42+
if args[0] == "config" {
43+
return server_Config(strings.TrimSpace(rest[1:]), user, room, flags)
44+
}
45+
3646
if args[0] == "set" {
3747

3848
args = args[1:]
@@ -287,3 +297,228 @@ func Server(rest string, user *users.UserRecord, room *rooms.Room, flags events.
287297

288298
return true, nil
289299
}
300+
301+
func server_Config(_ string, user *users.UserRecord, room *rooms.Room, flags events.EventFlag) (bool, error) {
302+
303+
// Get if already exists, otherwise create new
304+
cmdPrompt, isNew := user.StartPrompt(`server config`, "")
305+
306+
if isNew {
307+
user.SendText(``)
308+
menuOptions, _ := getConfigOptions("")
309+
tplTxt, _ := templates.Process("tables/numbered-list", menuOptions, user.UserId)
310+
user.SendText(tplTxt)
311+
}
312+
313+
configPrefix := ""
314+
if selection, ok := cmdPrompt.Recall("config-selected"); ok {
315+
configPrefix = selection.(string)
316+
}
317+
318+
if configPrefix != "" {
319+
allConfigData := configs.GetConfig().AllConfigData()
320+
if configVal, ok := allConfigData[configPrefix]; ok {
321+
322+
if !isEditAllowed(configPrefix) {
323+
user.SendText(errValueLocked.Error())
324+
user.ClearPrompt()
325+
return true, nil
326+
}
327+
328+
question := cmdPrompt.Ask(fmt.Sprintf(newValuePrompt, configPrefix), []string{fmt.Sprintf("%v", configVal)}, fmt.Sprintf("%v", configVal))
329+
if !question.Done {
330+
return true, nil
331+
}
332+
333+
user.ClearPrompt()
334+
335+
err := configs.SetVal(configPrefix, question.Response)
336+
if err == nil {
337+
allConfigData := configs.GetConfig().AllConfigData()
338+
user.SendText(``)
339+
user.SendText(fmt.Sprintf(`<ansi fg="6">%s</ansi> has been set to: <ansi fg="9">%s<ansi>`, configPrefix, allConfigData[configPrefix]))
340+
user.SendText(``)
341+
return true, nil
342+
}
343+
user.SendText(err.Error())
344+
return true, nil
345+
}
346+
}
347+
348+
question := cmdPrompt.Ask(`Choose a config option, or "quit":`, []string{``}, ``)
349+
if !question.Done {
350+
return true, nil
351+
}
352+
353+
if question.Response == "quit" {
354+
user.SendText("Quitting...")
355+
user.ClearPrompt()
356+
return true, nil
357+
}
358+
359+
fullPath := strings.ToLower(configPrefix)
360+
if fullPath != `` {
361+
fullPath += "."
362+
}
363+
fullPath += question.Response
364+
365+
if !isEditAllowed(fullPath) {
366+
user.SendText(errValueLocked.Error())
367+
question.RejectResponse()
368+
return true, nil
369+
}
370+
371+
menuOptions, ok := getConfigOptions(fullPath)
372+
if !ok {
373+
question.RejectResponse()
374+
menuOptions, _ = getConfigOptions("")
375+
fullPath = strings.ToLower(configPrefix)
376+
} else {
377+
378+
if len(menuOptions) == 1 {
379+
fullPath = menuOptions[0].Id.(string)
380+
381+
cmdPrompt.Store("config-selected", fullPath)
382+
383+
if !isEditAllowed(fullPath) {
384+
user.SendText(errValueLocked.Error())
385+
user.ClearPrompt()
386+
return true, nil
387+
}
388+
389+
allConfigData := configs.GetConfig().AllConfigData()
390+
if configVal, ok := allConfigData[fullPath]; ok {
391+
392+
cmdPrompt.Ask(fmt.Sprintf(newValuePrompt, fullPath), []string{fmt.Sprintf("%v", configVal)}, fmt.Sprintf("%v", configVal))
393+
return true, nil
394+
}
395+
}
396+
397+
cmdPrompt.Store("config-selected", fullPath)
398+
}
399+
400+
if fullPath != "" {
401+
user.SendText(``)
402+
user.SendText(` [<ansi fg="6">` + fullPath + `</ansi>]`)
403+
}
404+
405+
tplTxt, _ := templates.Process("tables/numbered-list", menuOptions, user.UserId)
406+
user.SendText(tplTxt)
407+
408+
question.RejectResponse()
409+
410+
return true, nil
411+
}
412+
413+
func isEditAllowed(configPath string) bool {
414+
415+
configPath = strings.ToLower(configPath)
416+
417+
if strings.HasSuffix(configPath, "locked") {
418+
return false
419+
}
420+
421+
sc := configs.GetServerConfig()
422+
for _, v := range sc.Locked {
423+
if strings.HasPrefix(configPath, strings.ToLower(v)) {
424+
return false
425+
}
426+
}
427+
428+
return true
429+
}
430+
431+
func getConfigOptions(input string) ([]templates.NameDescription, bool) {
432+
433+
input = strings.ToLower(input)
434+
435+
configOptions := []templates.NameDescription{}
436+
437+
allConfigData := configs.GetConfig().AllConfigData()
438+
pathLookup := map[string]string{}
439+
for name, _ := range allConfigData {
440+
441+
lowerName := strings.ToLower(name)
442+
pathLookup[lowerName] = name
443+
444+
builtPath := ""
445+
for _, namePart := range strings.Split(name, ".") {
446+
builtPath += namePart
447+
if _, ok := pathLookup[builtPath]; !ok {
448+
pathLookup[strings.ToLower(builtPath)] = builtPath
449+
}
450+
builtPath += "."
451+
}
452+
}
453+
454+
inputProperCase := input
455+
if caseCheck, ok := pathLookup[input]; ok {
456+
457+
inputProperCase = caseCheck
458+
459+
// Is this a full config path?
460+
if configVal, ok := allConfigData[inputProperCase]; ok {
461+
462+
configOptions = append(configOptions, templates.NameDescription{
463+
Id: inputProperCase,
464+
Name: inputProperCase,
465+
Description: fmt.Sprintf("%v", configVal),
466+
})
467+
468+
return configOptions, true
469+
470+
}
471+
472+
} else if input != "" {
473+
return configOptions, false
474+
}
475+
476+
// Find which partial path we are on and populate options
477+
usedNames := map[string]struct{}{}
478+
for fullConfigPath, configVal := range allConfigData {
479+
480+
if input != "" {
481+
if len(fullConfigPath) <= len(input) || fullConfigPath[0:len(inputProperCase)] != inputProperCase {
482+
continue
483+
}
484+
}
485+
486+
nextConfigPathSection := fullConfigPath
487+
if len(inputProperCase) > 0 {
488+
nextConfigPathSection = nextConfigPathSection[len(inputProperCase)+1:]
489+
}
490+
491+
desc := "..."
492+
if dotIdx := strings.Index(nextConfigPathSection, "."); dotIdx != -1 {
493+
nextConfigPathSection = nextConfigPathSection[:dotIdx]
494+
} else {
495+
desc = fmt.Sprintf("%v", configVal)
496+
}
497+
498+
if _, ok := usedNames[nextConfigPathSection]; ok {
499+
continue
500+
}
501+
502+
usedNames[nextConfigPathSection] = struct{}{}
503+
504+
pathWithSection := nextConfigPathSection
505+
if len(inputProperCase) > 0 {
506+
pathWithSection = inputProperCase + "." + pathWithSection
507+
}
508+
509+
configOptions = append(configOptions, templates.NameDescription{
510+
Id: pathWithSection,
511+
Name: nextConfigPathSection,
512+
Description: desc,
513+
})
514+
515+
}
516+
517+
if len(configOptions) > 0 {
518+
sort.Slice(configOptions, func(i, j int) bool {
519+
return configOptions[i].Name < configOptions[j].Name
520+
})
521+
}
522+
523+
return configOptions, true
524+
}

0 commit comments

Comments
 (0)