Skip to content

Commit 80ac77a

Browse files
authored
Merge pull request #85 from Chr1Z93/arg-savedobj
Added a "Make Saved Object" parameter
2 parents 77d4d82 + 7dfec3b commit 80ac77a

6 files changed

Lines changed: 180 additions & 5 deletions

File tree

file/jsonops.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@ type JSONReader interface {
2323
type JSONWriter interface {
2424
WriteObj(map[string]interface{}, string) error
2525
WriteObjArray([]map[string]interface{}, string) error
26+
WriteSavedObj(map[string]interface{}, string) error
27+
}
28+
29+
type SavedObject struct {
30+
SaveName string
31+
Date string
32+
VersionNumber string
33+
GameMode string
34+
GameType string
35+
GameComplexity string
36+
Tags []string
37+
Gravity float64
38+
PlayArea float64
39+
Table string
40+
Sky string
41+
Note string
42+
TabStates map[string]interface{}
43+
LuaScript string
44+
LuaScriptState string
45+
XmlUI string
46+
ObjectStates []map[string]interface{} // This will hold the original `m`
2647
}
2748

2849
// NewJSONOps initializes our object on a directory
@@ -80,6 +101,50 @@ func (j *JSONOps) WriteObj(m map[string]interface{}, filename string) error {
80101
return os.WriteFile(p, b, 0644)
81102
}
82103

104+
// WriteSavedObj writes a serialized JSON object to a file with the boilerplate for TTS saved objects.
105+
// For additional information, see saved-object-feature.md in the project repository.
106+
func (j *JSONOps) WriteSavedObj(m map[string]interface{}, filename string) error {
107+
var b []byte
108+
var err error
109+
110+
savedObject := SavedObject{
111+
SaveName: "",
112+
Date: "",
113+
VersionNumber: "",
114+
GameMode: "",
115+
GameType: "",
116+
GameComplexity: "",
117+
Tags: []string{},
118+
Gravity: 0.5,
119+
PlayArea: 0.5,
120+
Table: "",
121+
Sky: "",
122+
Note: "",
123+
TabStates: map[string]interface{}{},
124+
LuaScript: "",
125+
LuaScriptState: "",
126+
XmlUI: "",
127+
ObjectStates: []map[string]interface{}{m},
128+
}
129+
130+
b, err = json.MarshalIndent(savedObject, "", " ")
131+
if err != nil {
132+
return err
133+
}
134+
135+
// end-of-file newline
136+
b = append(b, '\n')
137+
138+
// ensure the path exists
139+
p := path.Join(j.basepath, filename)
140+
err = os.MkdirAll(path.Dir(p), 0750)
141+
if err != nil && !os.IsExist(err) {
142+
return fmt.Errorf("MkdirAll(%s): %v", path.Dir(p), err)
143+
}
144+
145+
return os.WriteFile(p, b, 0644)
146+
}
147+
83148
// WriteObjArray writes an array of serialized json objects to a file.
84149
func (j *JSONOps) WriteObjArray(m []map[string]interface{}, filename string) error {
85150
b, err := json.MarshalIndent(m, "", " ")

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var (
2121
modfile = flag.String("modfile", "", "where to read from when reversing.")
2222
objin = flag.String("objin", "", "if non-empty, don't build/reverse a full mod, only an object state array")
2323
objout = flag.String("objout", "", "if building only object state list, output to this filename")
24+
savedobj = flag.Bool("savedobj", false, "if present, will add the boiler plate for TTS to recognize as saved object.")
2425
)
2526

2627
var (
@@ -135,6 +136,7 @@ func main() {
135136
RootRead: rootops,
136137
RootWrite: outputOps,
137138
OnlyObjStates: OnlyObjStates,
139+
SavedObj: *savedobj,
138140
}
139141
err := m.GenerateFromConfig()
140142
if err != nil {

mod/generate.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type Mod struct {
3737
Modsettings file.JSONReader
3838
Objs file.JSONReader
3939
Objdirs file.DirExplorer
40+
SavedObj bool
4041

4142
// If not-empty: this holds the root filename for the object state json object
4243
OnlyObjStates string
@@ -148,7 +149,11 @@ func (m *Mod) generate(raw types.J) error {
148149

149150
// Print outputs internal representation of mod to json file with indents
150151
func (m *Mod) Print(basename string) error {
151-
return m.RootWrite.WriteObj(m.Data, basename)
152+
if m.SavedObj {
153+
return m.RootWrite.WriteSavedObj(m.Data, basename)
154+
} else {
155+
return m.RootWrite.WriteObj(m.Data, basename)
156+
}
152157
}
153158

154159
func tryPut(d *types.J, from, to string, fun func(string) (interface{}, error)) {

mod/generate_test.go

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ func TestGenerate(t *testing.T) {
1414
name string
1515
inputRoot types.J
1616
inputModSettings map[string]interface{}
17-
inputOjbs map[string]types.J
17+
inputObjs map[string]types.J
1818
inputLuaSrc map[string]string
1919
inputObjTexts map[string]string
20+
flags map[string]interface{}
2021
want map[string]interface{}
2122
}{
2223
{
@@ -63,7 +64,7 @@ func TestGenerate(t *testing.T) {
6364
inputLuaSrc: map[string]string{
6465
"parent/eda22b/childstate2.ttslua": "var foo = 42\nvar foo = 42\nvar foo = 42\nvar foo = 42\nvar foo = 42\nvar foo = 42\nvar foo = 42\nvar foo = 42\n",
6566
},
66-
inputOjbs: map[string]types.J{
67+
inputObjs: map[string]types.J{
6768
"parent.json": map[string]interface{}{
6869
"GUID": "parent",
6970
"States_path": map[string]interface{}{
@@ -134,7 +135,7 @@ func TestGenerate(t *testing.T) {
134135
"ObjectStates_order": []interface{}{"parent"},
135136
},
136137
inputLuaSrc: map[string]string{},
137-
inputOjbs: map[string]types.J{
138+
inputObjs: map[string]types.J{
138139
"parent.json": map[string]interface{}{
139140
"GUID": "parent",
140141
"States_path": map[string]interface{}{
@@ -195,6 +196,43 @@ func TestGenerate(t *testing.T) {
195196
"DecalPallet": nil,
196197
},
197198
},
199+
{
200+
name: "Saved Object (Simple)",
201+
inputObjs: map[string]types.J{
202+
"test123.json": map[string]interface{}{
203+
"GUID": "test123",
204+
"Description": "A test object",
205+
},
206+
},
207+
flags: map[string]interface{}{
208+
"OnlyObjStates": true,
209+
"SavedObj": true,
210+
},
211+
want: map[string]interface{}{
212+
"SaveName": "",
213+
"Date": "",
214+
"VersionNumber": "",
215+
"GameMode": "",
216+
"GameType": "",
217+
"GameComplexity": "",
218+
"Tags": []any{},
219+
"Gravity": 0.5,
220+
"PlayArea": 0.5,
221+
"Table": "",
222+
"Sky": "",
223+
"Note": "",
224+
"TabStates": map[string]interface{}{},
225+
"LuaScript": "",
226+
"LuaScriptState": "",
227+
"XmlUI": "",
228+
"ObjectStates": []interface{}{
229+
map[string]interface{}{
230+
"GUID": "test123",
231+
"Description": "A test object",
232+
},
233+
},
234+
},
235+
},
198236
} {
199237
t.Run(tc.name, func(t *testing.T) {
200238
rootff := &tests.FakeFiles{
@@ -207,7 +245,7 @@ func TestGenerate(t *testing.T) {
207245
}
208246
msff := &tests.FakeFiles{}
209247
objs := &tests.FakeFiles{
210-
Data: tc.inputOjbs,
248+
Data: tc.inputObjs,
211249
Fs: tc.inputObjTexts,
212250
}
213251
m := Mod{
@@ -218,6 +256,12 @@ func TestGenerate(t *testing.T) {
218256
Objs: objs,
219257
Objdirs: objs,
220258
}
259+
if OnlyObjStatesFlag, ok := tc.flags["OnlyObjStates"]; ok && OnlyObjStatesFlag == true {
260+
m.OnlyObjStates = "test123.json"
261+
}
262+
if savedObjFlag, ok := tc.flags["SavedObj"]; ok && savedObjFlag == true {
263+
m.SavedObj = true
264+
}
221265
err := m.GenerateFromConfig()
222266
if err != nil {
223267
t.Fatalf("Error reading config %v", err)

saved-object-feature.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# TTS Mod Manager (TTSMM) - Savegame Handling Documentation
2+
3+
## Overview
4+
5+
The TTS Mod Manager (TTSMM) is a tool designed to manage savegames from **Tabletop Simulator (TTS)**. TTS savegames are large JSON files that encapsulate the entire state of a game session, including all objects, settings, and scripts. Due to their size and complexity, these files are not well-suited for source control systems like GitHub.
6+
7+
TTSMM provides functionality to split savegames into separate files for individual objects and reconstruct them as needed. This makes it easier to manage and version control TTS savegames.
8+
9+
## Feature: "Saved Object" Output
10+
11+
In addition to handling regular savegames, TTSMM supports generating "Saved Object" files. Saved Objects are a special type of savegame in TTS with the same overall structure as regular savegames, but with many of the outer-layer fields left empty. These files are typically used to save and share individual objects or small groups of objects independently of a full savegame.
12+
13+
### Key Differences: Regular Savegames vs. Saved Objects
14+
15+
| Field | Regular Savegame Value | Saved Object Value |
16+
| ---------------- | -------------------------- | --------------------- |
17+
| `SaveName` | Populated with save name | Empty (`""`) |
18+
| `Date` | Timestamp of save creation | Empty (`""`) |
19+
| `VersionNumber` | Current TTS version | Empty (`""`) |
20+
| `GameMode` | Game mode description | Empty (`""`) |
21+
| `GameType` | Type of game | Empty (`""`) |
22+
| `GameComplexity` | Complexity descriptor | Empty (`""`) |
23+
| `Tags` | Array of tags | Empty (`[]`) |
24+
| `Gravity` | Physics gravity setting | Default (`0.5`) |
25+
| `PlayArea` | Play area scale | Default (`0.5`) |
26+
| `Table` | Table model used | Empty (`""`) |
27+
| `Sky` | Skybox setting | Empty (`""`) |
28+
| `Note` | Session note | Empty (`""`) |
29+
| `TabStates` | Tab states information | Empty (`{}`) |
30+
| `LuaScript` | Global Lua script content | Empty (`""`) |
31+
| `LuaScriptState` | Lua script state data | Empty (`""`) |
32+
| `XmlUI` | XML UI data | Empty (`""`) |
33+
| `ObjectStates` | Array of game objects | Array of game objects |

tests/fakefiles.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,32 @@ func (f *FakeFiles) WriteObjArray(data []map[string]interface{}, path string) er
7878
return nil
7979
}
8080

81+
// WriteSavedObj satisfies JSONWriter and mimics the behavior of the real WriteSavedObj.
82+
func (f *FakeFiles) WriteSavedObj(data map[string]interface{}, path string) error {
83+
savedObject := map[string]interface{}{
84+
"SaveName": "",
85+
"Date": "",
86+
"VersionNumber": "",
87+
"GameMode": "",
88+
"GameType": "",
89+
"GameComplexity": "",
90+
"Tags": []any{},
91+
"Gravity": 0.5,
92+
"PlayArea": 0.5,
93+
"Table": "",
94+
"Sky": "",
95+
"Note": "",
96+
"TabStates": map[string]interface{}{},
97+
"LuaScript": "",
98+
"LuaScriptState": "",
99+
"XmlUI": "",
100+
"ObjectStates": []map[string]interface{}{data},
101+
}
102+
103+
f.Data[path] = savedObject
104+
return nil
105+
}
106+
81107
// EncodeToFile satisfies LuaWriter
82108
func (f *FakeFiles) EncodeToFile(script, file string) error {
83109
f.Fs[file] = script

0 commit comments

Comments
 (0)