Skip to content

Commit 55240c5

Browse files
authored
Auto-Migration (#406)
# Description This defines a basic process for including auto-correcting/updating of datafiles etc. when users upgrade to a new binary version. ## Changes - main.go now defines VERSION - this should match release versions - internal/migration/ code foor migration purposes. - migration code is invoked in main.go before datafiles etc. are loaded (except for config) - fixes a small error in training room script - migration creates a backup of datafiles and restores on any errors - flag `--version` can be used when running gomud to display version and quit - version set to `0.9.1`
1 parent 4539dd0 commit 55240c5

File tree

14 files changed

+635
-67
lines changed

14 files changed

+635
-67
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11

2+
23
.DEFAULT_GOAL := build
34

45
VERSION ?= $(shell git rev-parse HEAD)
@@ -120,7 +121,7 @@ coverage:
120121
rm -rf bin
121122

122123
.PHONY: js-lint
123-
js-lint:
124+
js-lint: ### Run Javascript linter
124125
# Grep filtering it to remove errors reported by docker image around npm packages
125126
# if "### errors" is found in the output, exits with an error code of 1
126127
# This should allow us to use it in CI/CD

_datafiles/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Server:
7878
# accidental changes that could break the game.
7979
Locked:
8080
- FilePaths
81+
- Server.CurrentVersion
8182
- Server.NextRoomId
8283
- Server.Seed
8384
- Server.OnLoginCommands

_datafiles/world/default/mobs/tutorial/scripts/58-training_dummy.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ function onDie(mob, room, eventDetails) {
44

55
room.SendText( mob.GetCharacterName(true) + " crumbles to dust." );
66

7-
room.GetMob(teacherMobId, true);
8-
9-
teacherMob.Command('say You did it! As you can see you gain <ansi fg="experience">experience points</ansi> for combat victories.');
10-
teacherMob.Command('say Now head <ansi fg="exit">west</ansi> to complete your training.', 2.0);
7+
var teacherMob = room.GetMob(teacherMobId, true);
8+
if ( teacherMob != null ) {
9+
teacherMob.Command('say You did it! As you can see you gain <ansi fg="experience">experience points</ansi> for combat victories.');
10+
teacherMob.Command('say Now head <ansi fg="exit">west</ansi> to complete your training.', 2.0);
11+
}
1112
}

_datafiles/world/empty/mobs/tutorial/scripts/58-training_dummy.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ function onDie(mob, room, eventDetails) {
44

55
room.SendText( mob.GetCharacterName(true) + " crumbles to dust." );
66

7-
teacherMob = room.GetMob(teacherMobId, true);
8-
9-
teacherMob.Command('say You did it! Head <ansi fg="exit">west</ansi> to complete your training.');
7+
var teacherMob = room.GetMob(teacherMobId, true);
8+
if ( teacherMob != null ) {
9+
teacherMob.Command('say You did it! As you can see you gain <ansi fg="experience">experience points</ansi> for combat victories.');
10+
teacherMob.Command('say Now head <ansi fg="exit">west</ansi> to complete your training.', 2.0);
11+
}
1012
}

internal/configs/config.server.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package configs
22

33
type Server struct {
44
MudName ConfigString `yaml:"MudName"` // Name of the MUD
5+
CurrentVersion ConfigString `yaml:"CurrentVersion"` // Current version this mud has been updated to
56
Seed ConfigSecret `yaml:"Seed"` // Seed that may be used for generating content
67
MaxCPUCores ConfigInt `yaml:"MaxCPUCores"` // How many cores to allow for multi-core operations
78
OnLoginCommands ConfigSliceString `yaml:"OnLoginCommands"` // Commands to run when a user logs in
@@ -26,6 +27,10 @@ func (s *Server) Validate() {
2627
s.MaxCPUCores = 0 // default
2728
}
2829

30+
if s.CurrentVersion == `` {
31+
s.CurrentVersion = `0.9.0` // If no version found, failover to a known version
32+
}
33+
2934
}
3035

3136
func GetServerConfig() Server {

internal/flags/flags.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,21 @@ import (
1111
"github.com/GoMudEngine/GoMud/internal/mudlog"
1212
)
1313

14-
func HandleFlags() {
14+
func HandleFlags(serverVersion string) {
15+
1516
var portsearch string
17+
var showVersion bool
1618

1719
flag.StringVar(&portsearch, "port-search", "", "Search for the first 10 open ports: -port-search=30000-40000")
20+
flag.BoolVar(&showVersion, "version", false, "Display the current binary version")
1821

1922
flag.Parse()
2023

24+
if showVersion {
25+
fmt.Println(serverVersion)
26+
os.Exit(0)
27+
}
28+
2129
if portsearch != `` {
2230
doPortSearch(portsearch)
2331
os.Exit(0)

internal/migration/0.9.1.go

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package migration
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"regexp"
8+
"strings"
9+
10+
"github.com/GoMudEngine/GoMud/internal/configs"
11+
"github.com/GoMudEngine/GoMud/internal/mudlog"
12+
"github.com/GoMudEngine/GoMud/internal/rooms"
13+
"gopkg.in/yaml.v2"
14+
)
15+
16+
// Description:
17+
// rooms.Room.ZoneConfig was removed when Zone data was migrated to zone-config.yaml in zone folders
18+
// This function loads all of the yaml files in the DATAFILES/world/*/rooms/* and looks for any ZoneConfig data.
19+
// If found, the data is moved to a zone-config.yaml file, and the ZoneConfig data in the Room datafile is removed.
20+
func migrate_RoomZoneConfig() error {
21+
22+
// This struct is how ZoneConfig looked as of 0.9.1
23+
// Since we will be upgrading an older version to this format, use a copy of the struct from that period
24+
// To ensure we aren't using a struct that has changed over time
25+
type zoneConfig_1_0_0 struct {
26+
Name string `yaml:"name,omitempty"`
27+
RoomId int `yaml:"roomid,omitempty"`
28+
MobAutoScale struct {
29+
Minimum int `yaml:"minimum,omitempty"` // level scaling minimum
30+
Maximum int `yaml:"maximum,omitempty"` // level scaling maximum
31+
} `yaml:"autoscale,omitempty"` // level scaling range if any
32+
Mutators []struct {
33+
MutatorId string `yaml:"mutatorid,omitempty"` // Short text that will uniquely identify this modifier ("dusty")
34+
SpawnedRound uint64 `yaml:"spawnedround,omitempty"` // Tracks when this mutator was created (useful for decay)
35+
DespawnedRound uint64 `yaml:"despawnedround,omitempty"` // Track when it decayed to nothing.
36+
} `yaml:"mutators,omitempty"`
37+
IdleMessages []string `yaml:"idlemessages,omitempty"` // list of messages that can be displayed to players in the zone, assuming a room has none defined
38+
MusicFile string `yaml:"musicfile,omitempty"` // background music to play when in this zone
39+
DefaultBiome string `yaml:"defaultbiome,omitempty"` // city, swamp etc. see biomes.go
40+
RoomIds map[int]struct{} `yaml:"-"` // Does not get written. Built dyanmically when rooms are loaded.
41+
}
42+
43+
c := configs.GetConfig()
44+
45+
worldfilesGlob := filepath.Join(string(c.FilePaths.DataFiles), "rooms", "*", "*.yaml")
46+
matches, err := filepath.Glob(worldfilesGlob)
47+
48+
if err != nil {
49+
return err
50+
}
51+
52+
existingZoneFiles := map[string]struct{}{}
53+
54+
// We only care about room files, so ###.yaml (possible negative)
55+
re := regexp.MustCompile(`^[\-0-9]+\.yaml$`)
56+
for _, path := range matches {
57+
58+
//
59+
// Must look like a room yaml file:
60+
// 1.yaml
61+
// 123.yaml
62+
// -83.yaml
63+
// etc.
64+
//
65+
66+
if !re.MatchString(filepath.Base(path)) {
67+
continue
68+
}
69+
70+
//
71+
// strip the filename form the room file and replace with zone-config.yaml
72+
// to get the path to the zone-config.yaml
73+
//
74+
zoneFilePath := filepath.Join(filepath.Dir(path), "zone-config.yaml")
75+
76+
//
77+
// The following checks whether the zone config file already exists
78+
// We will leave the config data in the room data file if the zone-config.yaml is already present.
79+
// It should be inert if present, since it is not unmarshalled into anything in current code.
80+
//
81+
82+
// Check whether zone file already is tracked as existing, if found, skip.
83+
if _, ok := existingZoneFiles[zoneFilePath]; ok {
84+
continue
85+
}
86+
87+
_, err = os.Stat(zoneFilePath)
88+
if err == nil {
89+
// Mark zone file as existing, skip further processing.
90+
existingZoneFiles[zoneFilePath] = struct{}{}
91+
continue
92+
}
93+
94+
//
95+
// End check for existing zone-config.yaml
96+
// After this point, we will unmarshal the yaml file into a generic map structure.
97+
// This allows us to examine the data in the yaml file, particularly the "zoneconfig" node
98+
// since the ZoneConfig field has been removed from the rooms.Room struct
99+
// We can de-populate the field, move it, and re-write the yaml back to the original room template file.
100+
// The downside to this method is that being a map, the fields will be read/written in a non-deterministic manner,
101+
// So the room yaml file field orders may be written in a random order.
102+
// Because of this, and as a final fix, we will finally marshal/unmarshal into the proper room struct from the map data
103+
// Allowing us to write the data in an expected ordered form.
104+
//
105+
106+
data, err := os.ReadFile(path)
107+
if err != nil {
108+
return err
109+
}
110+
111+
//
112+
// First do a simple check for the field name in the text file.
113+
// We know the way the field will appear: "zoneconfig:"
114+
// This avoids having to unmarshal the struct and search that way, unnecessarily.
115+
//
116+
if !strings.Contains(string(data), "zoneconfig:") {
117+
continue
118+
}
119+
120+
//
121+
// Unmarshal the entire yaml file into a map
122+
// This will let us further examine the data, modify it, etc.
123+
//
124+
filedata := map[string]any{}
125+
err = yaml.Unmarshal(data, &filedata)
126+
if err != nil {
127+
return fmt.Errorf("failed to parse YAML: %w", err)
128+
}
129+
130+
// Make sure that the zoneconfig key is present and populated
131+
if filedata[`zoneconfig`] == nil {
132+
continue
133+
}
134+
135+
mudlog.Info("Migration 0.9.1", "file", path, "message", "migrating zoneconfig from room data file to zone-config.yaml")
136+
137+
//
138+
// From here on out, this code migrates zoneconfig data out of room file and into zone-config.yaml
139+
//
140+
roomFileInfo, _ := os.Stat(path)
141+
142+
mudlog.Info("Migration 0.9.1", "file", path, "message", "isolating zoneconfig data")
143+
144+
//
145+
// Isolate the zoneconfig and write it to its own zone-config.yaml file
146+
// We'll marshal just the zoneconfig data, get its bytes, then unmarshal it into
147+
// the desired target structure.
148+
// Some fields have changed or are missing due to some slight differences in the new struct
149+
// so we'll also try and reconcile some of that by pulling from the core room definition
150+
//
151+
zoneBytes, err := yaml.Marshal(filedata[`zoneconfig`])
152+
if err != nil {
153+
return err
154+
}
155+
156+
zoneDataStruct := zoneConfig_1_0_0{}
157+
158+
if err = yaml.Unmarshal(zoneBytes, &zoneDataStruct); err != nil {
159+
return err
160+
}
161+
162+
if filedata[`zone`] != nil {
163+
if zoneName, ok := filedata[`zone`].(string); ok {
164+
zoneDataStruct.Name = zoneName
165+
} else {
166+
zoneDataStruct.Name = filedata[`title`].(string)
167+
}
168+
169+
if defaultBiome, ok := filedata[`biome`].(string); ok {
170+
zoneDataStruct.DefaultBiome = defaultBiome
171+
}
172+
}
173+
174+
mudlog.Info("Migration 0.9.1", "file", path, "message", "writing "+zoneFilePath)
175+
176+
//
177+
// Write the zone data to the zone-config.yaml path
178+
// We'll just use whatever permissions were set in the room file for this file.
179+
//
180+
zoneFileBytes, err := yaml.Marshal(zoneDataStruct)
181+
if err != nil {
182+
return err
183+
}
184+
if err := os.WriteFile(zoneFilePath, zoneFileBytes, roomFileInfo.Mode().Perm()); err != nil {
185+
return err
186+
}
187+
188+
// Mark zone file as existing
189+
existingZoneFiles[zoneFilePath] = struct{}{}
190+
191+
mudlog.Info("Migration 0.9.1", "file", path, "message", "writing modified room data")
192+
193+
//
194+
// Now clear the "zoneconfig" node from the room data.
195+
// The data will be in a random order if we just write this back to the room yaml file,
196+
// so we'll take the extract step of marshalling the room data from the map into a string,
197+
// and then unmarshal it into the actual target rooms.Room{} struct.
198+
// This way, when writing to a file, it'll be in the typical field order according to the struct
199+
// field order.
200+
//
201+
delete(filedata, `zoneconfig`)
202+
203+
// First marshal the modified room data into bytes
204+
modifiedRoomBytes, err := yaml.Marshal(filedata)
205+
if err != nil {
206+
return err
207+
}
208+
209+
// Unmarshal the bytes into the proper struct
210+
modifiedRoomStruct := rooms.Room{}
211+
if err = yaml.Unmarshal(modifiedRoomBytes, &modifiedRoomStruct); err != nil {
212+
return err
213+
}
214+
215+
// Marshal again, this time using the proper struct
216+
modifiedRoomBytes, err = yaml.Marshal(modifiedRoomStruct)
217+
if err != nil {
218+
return err
219+
}
220+
221+
// Again, we'll just use the rooms original permissions when writing.
222+
if err := os.WriteFile(path, modifiedRoomBytes, roomFileInfo.Mode().Perm()); err != nil {
223+
return err
224+
}
225+
226+
mudlog.Info("Migration 0.9.1", "file", path, "message", "successfully updated")
227+
228+
}
229+
230+
return nil
231+
}

internal/migration/backup.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package migration
2+
3+
import (
4+
"errors"
5+
"io"
6+
"io/fs"
7+
"os"
8+
"path/filepath"
9+
10+
"github.com/GoMudEngine/GoMud/internal/configs"
11+
)
12+
13+
func datafilesBackup() (string, error) {
14+
15+
tmpDir, err := os.MkdirTemp("", "datafiles_backup_*")
16+
if err != nil {
17+
return "", err
18+
}
19+
20+
c := configs.GetConfig()
21+
datafilesFolder := string(c.FilePaths.DataFiles)
22+
23+
err = copyDir(datafilesFolder, tmpDir)
24+
if err != nil {
25+
return "", err
26+
}
27+
28+
return tmpDir, nil
29+
}
30+
31+
func copyDir(src string, dst string) error {
32+
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
33+
if err != nil {
34+
return err
35+
}
36+
37+
relPath, err := filepath.Rel(src, path)
38+
if err != nil {
39+
return err
40+
}
41+
42+
destPath := filepath.Join(dst, relPath)
43+
44+
if d.IsDir() {
45+
_, err := os.Stat(destPath)
46+
if errors.Is(err, os.ErrNotExist) {
47+
return os.MkdirAll(destPath, 0755)
48+
}
49+
return nil
50+
}
51+
52+
// It’s a file
53+
return copyFile(path, destPath)
54+
})
55+
}
56+
57+
// CopyFile copies a single file
58+
func copyFile(srcFile, dstFile string) error {
59+
srcF, err := os.Open(srcFile)
60+
if err != nil {
61+
return err
62+
}
63+
defer srcF.Close()
64+
65+
dstF, err := os.Create(dstFile)
66+
if err != nil {
67+
return err
68+
}
69+
defer dstF.Close()
70+
71+
_, err = io.Copy(dstF, srcF)
72+
return err
73+
}

0 commit comments

Comments
 (0)