Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a "Make Saved Object" parameter #85

Merged
merged 1 commit into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions file/jsonops.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ type JSONReader interface {
type JSONWriter interface {
WriteObj(map[string]interface{}, string) error
WriteObjArray([]map[string]interface{}, string) error
WriteSavedObj(map[string]interface{}, string) error
}

type SavedObject struct {
SaveName string
Date string
VersionNumber string
GameMode string
GameType string
GameComplexity string
Tags []string
Gravity float64
PlayArea float64
Table string
Sky string
Note string
TabStates map[string]interface{}
LuaScript string
LuaScriptState string
XmlUI string
ObjectStates []map[string]interface{} // This will hold the original `m`
}

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

// WriteSavedObj writes a serialized JSON object to a file with the boilerplate for TTS saved objects.
// For additional information, see saved-object-feature.md in the project repository.
func (j *JSONOps) WriteSavedObj(m map[string]interface{}, filename string) error {
var b []byte
var err error

savedObject := SavedObject{
SaveName: "",
Date: "",
VersionNumber: "",
GameMode: "",
GameType: "",
GameComplexity: "",
Tags: []string{},
Gravity: 0.5,
PlayArea: 0.5,
Table: "",
Sky: "",
Note: "",
TabStates: map[string]interface{}{},
LuaScript: "",
LuaScriptState: "",
XmlUI: "",
ObjectStates: []map[string]interface{}{m},
}

b, err = json.MarshalIndent(savedObject, "", " ")
if err != nil {
return err
}

// end-of-file newline
b = append(b, '\n')

// ensure the path exists
p := path.Join(j.basepath, filename)
err = os.MkdirAll(path.Dir(p), 0750)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("MkdirAll(%s): %v", path.Dir(p), err)
}

return os.WriteFile(p, b, 0644)
}

// WriteObjArray writes an array of serialized json objects to a file.
func (j *JSONOps) WriteObjArray(m []map[string]interface{}, filename string) error {
b, err := json.MarshalIndent(m, "", " ")
Expand Down
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var (
modfile = flag.String("modfile", "", "where to read from when reversing.")
objin = flag.String("objin", "", "if non-empty, don't build/reverse a full mod, only an object state array")
objout = flag.String("objout", "", "if building only object state list, output to this filename")
savedobj = flag.Bool("savedobj", false, "if present, will add the boiler plate for TTS to recognize as saved object.")
)

var (
Expand Down Expand Up @@ -135,6 +136,7 @@ func main() {
RootRead: rootops,
RootWrite: outputOps,
OnlyObjStates: OnlyObjStates,
SavedObj: *savedobj,
}
err := m.GenerateFromConfig()
if err != nil {
Expand Down
7 changes: 6 additions & 1 deletion mod/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type Mod struct {
Modsettings file.JSONReader
Objs file.JSONReader
Objdirs file.DirExplorer
SavedObj bool

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

// Print outputs internal representation of mod to json file with indents
func (m *Mod) Print(basename string) error {
return m.RootWrite.WriteObj(m.Data, basename)
if m.SavedObj {
return m.RootWrite.WriteSavedObj(m.Data, basename)
} else {
return m.RootWrite.WriteObj(m.Data, basename)
}
}

func tryPut(d *types.J, from, to string, fun func(string) (interface{}, error)) {
Expand Down
52 changes: 48 additions & 4 deletions mod/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ func TestGenerate(t *testing.T) {
name string
inputRoot types.J
inputModSettings map[string]interface{}
inputOjbs map[string]types.J
inputObjs map[string]types.J
inputLuaSrc map[string]string
inputObjTexts map[string]string
flags map[string]interface{}
want map[string]interface{}
}{
{
Expand Down Expand Up @@ -63,7 +64,7 @@ func TestGenerate(t *testing.T) {
inputLuaSrc: map[string]string{
"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",
},
inputOjbs: map[string]types.J{
inputObjs: map[string]types.J{
"parent.json": map[string]interface{}{
"GUID": "parent",
"States_path": map[string]interface{}{
Expand Down Expand Up @@ -134,7 +135,7 @@ func TestGenerate(t *testing.T) {
"ObjectStates_order": []interface{}{"parent"},
},
inputLuaSrc: map[string]string{},
inputOjbs: map[string]types.J{
inputObjs: map[string]types.J{
"parent.json": map[string]interface{}{
"GUID": "parent",
"States_path": map[string]interface{}{
Expand Down Expand Up @@ -195,6 +196,43 @@ func TestGenerate(t *testing.T) {
"DecalPallet": nil,
},
},
{
name: "Saved Object (Simple)",
inputObjs: map[string]types.J{
"test123.json": map[string]interface{}{
"GUID": "test123",
"Description": "A test object",
},
},
flags: map[string]interface{}{
"OnlyObjStates": true,
"SavedObj": true,
},
want: map[string]interface{}{
"SaveName": "",
"Date": "",
"VersionNumber": "",
"GameMode": "",
"GameType": "",
"GameComplexity": "",
"Tags": []any{},
"Gravity": 0.5,
"PlayArea": 0.5,
"Table": "",
"Sky": "",
"Note": "",
"TabStates": map[string]interface{}{},
"LuaScript": "",
"LuaScriptState": "",
"XmlUI": "",
"ObjectStates": []interface{}{
map[string]interface{}{
"GUID": "test123",
"Description": "A test object",
},
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
rootff := &tests.FakeFiles{
Expand All @@ -207,7 +245,7 @@ func TestGenerate(t *testing.T) {
}
msff := &tests.FakeFiles{}
objs := &tests.FakeFiles{
Data: tc.inputOjbs,
Data: tc.inputObjs,
Fs: tc.inputObjTexts,
}
m := Mod{
Expand All @@ -218,6 +256,12 @@ func TestGenerate(t *testing.T) {
Objs: objs,
Objdirs: objs,
}
if OnlyObjStatesFlag, ok := tc.flags["OnlyObjStates"]; ok && OnlyObjStatesFlag == true {
m.OnlyObjStates = "test123.json"
}
if savedObjFlag, ok := tc.flags["SavedObj"]; ok && savedObjFlag == true {
m.SavedObj = true
}
err := m.GenerateFromConfig()
if err != nil {
t.Fatalf("Error reading config %v", err)
Expand Down
33 changes: 33 additions & 0 deletions saved-object-feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# TTS Mod Manager (TTSMM) - Savegame Handling Documentation

## Overview

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.

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.

## Feature: "Saved Object" Output

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.

### Key Differences: Regular Savegames vs. Saved Objects

| Field | Regular Savegame Value | Saved Object Value |
| ---------------- | -------------------------- | --------------------- |
| `SaveName` | Populated with save name | Empty (`""`) |
| `Date` | Timestamp of save creation | Empty (`""`) |
| `VersionNumber` | Current TTS version | Empty (`""`) |
| `GameMode` | Game mode description | Empty (`""`) |
| `GameType` | Type of game | Empty (`""`) |
| `GameComplexity` | Complexity descriptor | Empty (`""`) |
| `Tags` | Array of tags | Empty (`[]`) |
| `Gravity` | Physics gravity setting | Default (`0.5`) |
| `PlayArea` | Play area scale | Default (`0.5`) |
| `Table` | Table model used | Empty (`""`) |
| `Sky` | Skybox setting | Empty (`""`) |
| `Note` | Session note | Empty (`""`) |
| `TabStates` | Tab states information | Empty (`{}`) |
| `LuaScript` | Global Lua script content | Empty (`""`) |
| `LuaScriptState` | Lua script state data | Empty (`""`) |
| `XmlUI` | XML UI data | Empty (`""`) |
| `ObjectStates` | Array of game objects | Array of game objects |
26 changes: 26 additions & 0 deletions tests/fakefiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,32 @@ func (f *FakeFiles) WriteObjArray(data []map[string]interface{}, path string) er
return nil
}

// WriteSavedObj satisfies JSONWriter and mimics the behavior of the real WriteSavedObj.
func (f *FakeFiles) WriteSavedObj(data map[string]interface{}, path string) error {
savedObject := map[string]interface{}{
"SaveName": "",
"Date": "",
"VersionNumber": "",
"GameMode": "",
"GameType": "",
"GameComplexity": "",
"Tags": []any{},
"Gravity": 0.5,
"PlayArea": 0.5,
"Table": "",
"Sky": "",
"Note": "",
"TabStates": map[string]interface{}{},
"LuaScript": "",
"LuaScriptState": "",
"XmlUI": "",
"ObjectStates": []map[string]interface{}{data},
}

f.Data[path] = savedObject
return nil
}

// EncodeToFile satisfies LuaWriter
func (f *FakeFiles) EncodeToFile(script, file string) error {
f.Fs[file] = script
Expand Down
Loading