Skip to content

Commit

Permalink
Merge pull request #45 from argonui/bundle
Browse files Browse the repository at this point in the history
Allow Bundler to write the required files it found to the src/ directory
  • Loading branch information
argonui authored Nov 21, 2022
2 parents b329729 + 0bd7edf commit 2ab4280
Show file tree
Hide file tree
Showing 13 changed files with 319 additions and 159 deletions.
30 changes: 19 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,50 @@ This golang library is intended to be able to convert source-controllable config
and luascript into a functioning json file that can be loaded into TTS as a
workshop mod.

# Getting the binary

The binary is built by slsa-framework/slsa-github-generator and can be found attached to the latest release, for example: https://github.com/argonui/TTSModManager/releases/tag/v0.2.4/TTSModManager.exe for windows. In the examples i'll refer to the exe, but you can use the TTSModManager-Liunux with the same expected behavior.

# Example Usage
## Generate json from a directory
$moddir = directory to read from

$config/output.json = file to output to
```
go run main.go --moddir="C:\Users\USER\Documents\Projects\MyProject"
TTSModManager.exe --moddir="C:\Users\USER\Documents\Projects\MyProject"
```

The finished json file is found in $moddir/output.json by default. if you'd like
to specify the output file you can use the `modfile` argument.

## Generate a directory from existing json file
$config = directory to write to
$moddir = directory to write to
$modfile = existing tts mod file to read from

$ttsmodfile = existing tts mod file to read from
```
go run main.go --reverse --moddir="C:\Users\USER\Documents\Projects\MyProject" --modfile="C:\Users\USER\Documents\My Games\Tabletop Simulator\Mods\Workshop\existingMod.json"
TTSModManager.exe --reverse --moddir="C:\Users\USER\Documents\Projects\MyProject" --modfile="C:\Users\USER\Documents\My Games\Tabletop Simulator\Mods\Workshop\existingMod.json"
```

If you'd like the bundled lua requirements to be written to the `src/` folder, pass `--writesrc`.

## Testing a TTS mod conversion
### reverse existing modfile into directory
$ttsmodfile = existing tts mod file to read from

$moddir = directory to write to
```
go run main.go --reverse --moddir="C:\Users\USER\Documents\Projects\MyProject" --modfile="C:\Users\USER\Documents\My Games\Tabletop Simulator\Mods\Workshop\existingMod.json"
TTSModManager.exe --reverse --moddir="C:\Users\USER\Documents\Projects\MyProject" --modfile="C:\Users\USER\Documents\My Games\Tabletop Simulator\Mods\Workshop\existingMod.json"
```

### generate a modfile based on directory
$moddir = directory to read from
```
go run main.go --moddir="C:\Users\USER\Documents\Projects\MyProject"
TTSModManager.exe --moddir="C:\Users\USER\Documents\Projects\MyProject"
```

### compare the original modfile with new generated modfile
$ttsmodfile = original tts mod file
## Running a local copy

If you are developing a feature and would like to run the tool, use this instead of `TTSModManager.exe`

$altmodfile = new generated modfile to compare to
```
go test . --modfile="C:\Users\USER\Documents\My Games\Tabletop Simulator\Mods\Workshop\existingMod.json" --altmodfile="C:\Users\USER\Documents\Projects\MyProject\output.json"
go run main.go --moddir="..."
```
71 changes: 59 additions & 12 deletions bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ end)(nil)`
funcprefixReplace string = `SRC_LOCATION`
funcprefix string = `__bundle_register("SRC_LOCATION", function(require, _LOADED, __bundle_register, __bundle_modules)`
funcsuffix string = `end)`
rootfuncname string = `__root`
// Rootname is the bundle name for the batch of raw lua
Rootname string = `__root`
)

// IsBundled keeps regex bundling logic to this file
Expand All @@ -69,20 +70,62 @@ func IsBundled(rawlua string) bool {
return false
}

// Unbundle takes luacode and strips it down to the root sub function
func Unbundle(rawlua string) (string, error) {
// UnbundleAll takes luacode generates all bundlenames and bundles
func UnbundleAll(rawlua string) (map[string]string, error) {
if !IsBundled(rawlua) {
return rawlua, nil
return map[string]string{Rootname: rawlua}, nil
}
scripts := map[string]string{}
r, err := findNextBundledScript(rawlua)
for r.leftover != "" {
if err != nil {
return nil, fmt.Errorf("findNextBundledScript(%s): %v", rawlua, err)
}
scripts[r.name] = r.body
r, err = findNextBundledScript(r.leftover)
}
// [return __bundle_require(\"__root\")|__bundle_register]
root := regexp.MustCompile(`(?s)__bundle_register\("__root", function\(require, _LOADED, __bundle_register, __bundle_modules\)[\r\n\s]+(.*?)[\r\n ]+end\)[\n\r]+(return __bundle_require\(\"__root\"\)|__bundle_register)+`)
matches := root.FindStringSubmatch(rawlua)
if _, ok := scripts[Rootname]; !ok {
return nil, fmt.Errorf("Failed to find root bundle")
}

return scripts, nil
}

if len(matches) <= 1 {
return "", fmt.Errorf("could not find root bundle")
type result struct {
name, body, leftover string
}

func findNextBundledScript(rawlua string) (result, error) {
root := regexp.MustCompile(`(?s)__bundle_register\("(.*?)", function\(require, _LOADED, __bundle_register, __bundle_modules\)[\r\n\s]+(.*?)[\r\n ]+end\)[\n\r]+(return __bundle_require\(\"__root\"\)|__bundle_register)+`)
m := root.FindStringSubmatchIndex(rawlua)
if m == nil {
return result{}, nil
}
if len(m) != 8 {
return result{}, fmt.Errorf("Expected 8 indices, got %v", m)
}
// first 2 ints are indices of entire match
// second 2 ints are indices of script name
// third 2 are script body
// fourth 2 are suffix needed to match
return result{
name: rawlua[m[2]:m[3]],
body: rawlua[m[4]:m[5]],
leftover: rawlua[m[6]:],
}, nil
}

return matches[1], nil
// Unbundle extracts the root bundle per
func Unbundle(rawlua string) (string, error) {
srcmap, err := UnbundleAll(rawlua)
if err != nil {
return "", err
}
rt, ok := srcmap[Rootname]
if !ok {
return "", fmt.Errorf("Rootname not found in unbundled map")
}
return rt, nil
}

// Bundle grabs all dependencies and creates a single luascript
Expand All @@ -91,9 +134,9 @@ func Bundle(rawlua string, l file.LuaReader) (string, error) {
return rawlua, nil
}
reqs := map[string]string{
rootfuncname: rawlua,
Rootname: rawlua,
}
todo := []string{rootfuncname}
todo := []string{Rootname}
for len(todo) > 0 {
fname := todo[0]
todo = todo[1:] // pop first element off
Expand All @@ -115,6 +158,10 @@ func Bundle(rawlua string, l file.LuaReader) (string, error) {
}
todo = append(todo, reqsToLoad...)
}
if len(reqs) == 1 {
// if there were no requires to load in, no need to bundle
return rawlua, nil
}

bundlestr := metaprefix + "\n"

Expand Down
26 changes: 13 additions & 13 deletions bundler/bundler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package bundler

import (
"fmt"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -249,17 +248,23 @@ require("core/AgendaDeck")
var a = '2'
require("core/AgendaDeck")
end)
__bundle_register("core/AgendaDeck", function(require, _LOADED, __bundle_register, __bundle_modules)
var b = '3'
end)
return __bundle_require("__root")
`
got, err := Unbundle(raw)
got, err := UnbundleAll(raw)
if err != nil {
t.Fatalf("expected no err, got %v", err)
}
want := `require("core/AgendaDeck")
want := map[string]string{
Rootname: `require("core/AgendaDeck")
var a = '2'
require("core/AgendaDeck")`
if want != got {
t.Errorf("want <%s>, got <%s>\n", want, got)
require("core/AgendaDeck")`,
"core/AgendaDeck": "var b = '3'",
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("want != got:\n%v\n", diff)
}
}

Expand Down Expand Up @@ -425,20 +430,15 @@ func TestBundleNoRequires(t *testing.T) {
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
want := strings.Trim(strings.Join(
[]string{metaprefix,
strings.Replace(funcprefix, funcprefixReplace, rootfuncname, 1),
"var foo = 42",
funcsuffix,
metasuffix}, "\n"), "\n\n")
want := `var foo = 42`

if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("want != got:\n%v\n", diff)
}
}

func TestFailedUnbundle(t *testing.T) {
rawlua := ` __bundle_register("core/AgendaDeck", function(require, _LOADED, __bundle_register, __bundle_modules)
rawlua := ` __bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
MIN_VALUE = -99
MAX_VALUE = 999
`
Expand Down
110 changes: 110 additions & 0 deletions handler/luahandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package handler

import (
"ModCreator/bundler"
"ModCreator/file"
"fmt"
)

// LuaHandler is needed because handling if a script should be written to src or to objects folder,
// and if it it long enough to be written to separate file at all has become
// burdensome. abstract into struct
type LuaHandler struct {
ObjWriter file.LuaWriter
SrcWriter file.LuaWriter
Reader file.LuaReader
}

// HandleAction describes at the end of handling,
// a single value should be written to a single key
type HandleAction struct {
Noop bool
Key string
Value string
}

// WhileReadingFromFile consolidates expected behavior of both objects and root
// json while reading lua from file.
func (lh *LuaHandler) WhileReadingFromFile(rawj map[string]interface{}) (HandleAction, error) {
rawscript := ""
if spraw, ok := rawj["LuaScript_path"]; ok {
sp, ok := spraw.(string)
if !ok {
return HandleAction{}, fmt.Errorf("Expected LuaScript_path to be type string, was %T", spraw)
}
encoded, err := lh.Reader.EncodeFromFile(sp)
if err != nil {
return HandleAction{}, fmt.Errorf("l.EncodeFromFile(%s) : %v", sp, err)
}
rawscript = encoded
}
if sraw, ok := rawj["LuaScript"]; ok {
s, ok := sraw.(string)
if !ok {
return HandleAction{}, fmt.Errorf("Expected LuaScript to be type string, was %T", sraw)
}
rawscript = s
}
bundled, err := bundler.Bundle(rawscript, lh.Reader)
if err != nil {
return HandleAction{}, fmt.Errorf("Bundle(%s): %v", rawscript, err)
}
if bundled == "" {
return HandleAction{
Noop: true,
}, nil
}

return HandleAction{
Key: "LuaScript",
Value: bundled,
Noop: false,
}, nil
}

// WhileWritingToFile consolidates the logic and flow of conditionally writing
// lua to a file, and which file to write to
func (lh *LuaHandler) WhileWritingToFile(rawj map[string]interface{}, possiblefname string) (HandleAction, error) {
rawscript, ok := rawj["LuaScript"]
if !ok {
return HandleAction{Noop: true}, nil
}
script, ok := rawscript.(string)
if !ok {
return HandleAction{}, fmt.Errorf("Value of content at LuaScript expected to be string, was %v, type %T",
rawscript, rawscript)
}

allScripts, err := bundler.UnbundleAll(script)
if err != nil {
return HandleAction{}, fmt.Errorf("UnbundleAll(...): %v", err)
}
// root bundle is promised to exist
rootscript, _ := allScripts[bundler.Rootname]
returnAction := HandleAction{Noop: false}
if len(rootscript) > 80 {
err = lh.ObjWriter.EncodeToFile(rootscript, possiblefname)
if err != nil {
return HandleAction{}, fmt.Errorf("EncodeToFile(<root script>, %s): %v", possiblefname, err)
}
returnAction.Key = "LuaScript_path"
returnAction.Value = possiblefname
} else {
returnAction.Key = "LuaScript"
returnAction.Value = rootscript
}
delete(allScripts, bundler.Rootname)

for k, script := range allScripts {
fname := fmt.Sprintf("%s.ttslua", k)
if lh.SrcWriter == nil {
break
}
err := lh.SrcWriter.EncodeToFile(script, fname)
if err != nil {
return HandleAction{}, fmt.Errorf("SrcWriter.EncodeToFile(<>, %s): %v", fname, err)
}
}

return returnAction, nil
}
11 changes: 8 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import (
)

var (
moddir = flag.String("moddir", "testdata/simple", "a directory containing tts mod configs")
rev = flag.Bool("reverse", false, "Instead of building a json from file structure, build file structure from json.")
modfile = flag.String("modfile", "", "where to read from when reversing.")
moddir = flag.String("moddir", "testdata/simple", "a directory containing tts mod configs")
rev = flag.Bool("reverse", false, "Instead of building a json from file structure, build file structure from json.")
writeToSrc = flag.Bool("writesrc", false, "When unbundling Lua, save the included 'require' files to the src/ directory.")
modfile = flag.String("modfile", "", "where to read from when reversing.")
)

const (
Expand All @@ -32,6 +33,7 @@ func main() {
[]string{path.Join(*moddir, textSubdir), path.Join(*moddir, objectsSubdir)},
path.Join(*moddir, objectsSubdir),
)
luaSrc := file.NewLuaOps(path.Join(*moddir, textSubdir))
ms := file.NewJSONOps(path.Join(*moddir, modsettingsDir))
objs := file.NewJSONOps(path.Join(*moddir, objectsSubdir))
objdir := file.NewDirOps(path.Join(*moddir, objectsSubdir))
Expand All @@ -49,6 +51,9 @@ func main() {
ObjDirCreeator: objdir,
RootWrite: rootops,
}
if *writeToSrc {
r.LuaSrcWriter = luaSrc
}
err = r.Write(raw)
if err != nil {
log.Fatalf("reverse.Write(<%s>) failed : %v", *modfile, err)
Expand Down
Loading

0 comments on commit 2ab4280

Please sign in to comment.