From 48c51f9d5639d62577c3986258a58fb4143f57d8 Mon Sep 17 00:00:00 2001 From: Jeremy Smitherman Date: Wed, 11 Feb 2026 01:16:36 -0600 Subject: [PATCH 1/4] extracted command type from dispatcher --- autosplitter/socket.go | 5 +- command/commands.go | 23 ++++++ config/service.go | 34 ++++----- config/service_test.go | 18 ++--- dispatcher/service.go | 45 ++++++------ dispatcher/service_test.go | 12 ++-- frontend/src/components/SkinPicker.tsx | 34 --------- frontend/wailsjs/go/dispatcher/Service.d.ts | 5 +- frontend/wailsjs/go/dispatcher/Service.js | 4 ++ opensplit.go | 2 +- repo/adapters/session.go | 24 +++++++ session/service.go | 79 +-------------------- session/splitfile.go | 77 ++++++++++++++++++++ statemachine/config.go | 29 ++++---- statemachine/editing.go | 9 +-- statemachine/newFile.go | 9 +-- statemachine/running.go | 37 +++++----- statemachine/service.go | 25 +++---- statemachine/welcome.go | 19 ++--- 19 files changed, 258 insertions(+), 232 deletions(-) create mode 100644 command/commands.go delete mode 100644 frontend/src/components/SkinPicker.tsx diff --git a/autosplitter/socket.go b/autosplitter/socket.go index 677a18d..46d4dd2 100644 --- a/autosplitter/socket.go +++ b/autosplitter/socket.go @@ -6,6 +6,7 @@ import ( "net" "sync" + "github.com/zellydev-games/opensplit/command" "github.com/zellydev-games/opensplit/dispatcher" "github.com/zellydev-games/opensplit/logger" ) @@ -95,7 +96,7 @@ func (s *Socket) Listen() { version := int(packet[4]) ackRequested := int(packet[5]) == 1 - command := dispatcher.Command(packet[6]) + c := command.Command(packet[6]) if version != 1 { logger.Errorf(logModule, "invalid version: %d", version) @@ -105,7 +106,7 @@ func (s *Socket) Listen() { continue } - _, err = s.dispatcher.Dispatch(command, nil) + _, err = s.dispatcher.Dispatch(c, nil) if err != nil { if ackRequested { sendAck(conn, addr, 2) diff --git a/command/commands.go b/command/commands.go new file mode 100644 index 0000000..6d09039 --- /dev/null +++ b/command/commands.go @@ -0,0 +1,23 @@ +package command + +// Command bytes are sent to the Service.Dispatch method receiver to indicate the state machine should take some action. +type Command byte + +const ( + QUIT Command = iota + NEW + LOAD + EDIT + CANCEL + SUBMIT + CLOSE + RESET + SAVE + SPLIT + UNDO + SKIP + PAUSE + TOGGLEGLOBAL + FOCUS + HELLO +) diff --git a/config/service.go b/config/service.go index 09b2286..10051a5 100644 --- a/config/service.go +++ b/config/service.go @@ -4,7 +4,7 @@ import ( "os" "sync" - "github.com/zellydev-games/opensplit/dispatcher" + "github.com/zellydev-games/opensplit/command" "github.com/zellydev-games/opensplit/keyinfo" "github.com/zellydev-games/opensplit/logger" ) @@ -14,12 +14,12 @@ const logModule = "config" // Service holds configuration options so that Service.GetEnvironment can work for both backend and frontend. type Service struct { mu sync.Mutex - SpeedRunAPIBase string `json:"speed_run_API_base"` - KeyConfig map[dispatcher.Command]keyinfo.KeyData `json:"key_config"` - GlobalHotkeysActive bool `json:"global_hotkeys_active"` - SplitFileDir string `json:"splitfile_dir"` - SkinsDir string `json:"skins_dir"` - SelectedSkin string `json:"selected_skin"` + SpeedRunAPIBase string `json:"speed_run_API_base"` + KeyConfig map[command.Command]keyinfo.KeyData `json:"key_config"` + GlobalHotkeysActive bool `json:"global_hotkeys_active"` + SplitFileDir string `json:"splitfile_dir"` + SkinsDir string `json:"skins_dir"` + SelectedSkin string `json:"selected_skin"` configUpdatedChannel chan<- *Service } @@ -29,7 +29,7 @@ func NewService(splitFileFir string, skinsDir string) (*Service, chan *Service) SplitFileDir: splitFileFir, SkinsDir: skinsDir, SpeedRunAPIBase: "", - KeyConfig: map[dispatcher.Command]keyinfo.KeyData{}, + KeyConfig: map[command.Command]keyinfo.KeyData{}, configUpdatedChannel: updateChannel, }, updateChannel } @@ -48,24 +48,24 @@ func (s *Service) GetEnvironment() *Service { } // UpdateKeyBinding changes the ConfigPayload for the given command. -func (s *Service) UpdateKeyBinding(command dispatcher.Command, data keyinfo.KeyData) { +func (s *Service) UpdateKeyBinding(c command.Command, data keyinfo.KeyData) { s.mu.Lock() defer s.mu.Unlock() - s.KeyConfig[command] = data + s.KeyConfig[c] = data s.sendUIBridgeUpdate() - logger.Infof(logModule, "updated key binding for command %v to %s", command, data.LocaleName) + logger.Infof(logModule, "updated key binding for c %v to %s", c, data.LocaleName) } // CreateDefaultConfig sets the service's options to reasonable defaults. // // Useful if the config file hasn't been created yet (first run) func (s *Service) CreateDefaultConfig() { - s.KeyConfig = map[dispatcher.Command]keyinfo.KeyData{} - s.KeyConfig[dispatcher.SPLIT] = keyinfo.KeyData{} - s.KeyConfig[dispatcher.UNDO] = keyinfo.KeyData{} - s.KeyConfig[dispatcher.SKIP] = keyinfo.KeyData{} - s.KeyConfig[dispatcher.PAUSE] = keyinfo.KeyData{} - s.KeyConfig[dispatcher.RESET] = keyinfo.KeyData{} + s.KeyConfig = map[command.Command]keyinfo.KeyData{} + s.KeyConfig[command.SPLIT] = keyinfo.KeyData{} + s.KeyConfig[command.UNDO] = keyinfo.KeyData{} + s.KeyConfig[command.SKIP] = keyinfo.KeyData{} + s.KeyConfig[command.PAUSE] = keyinfo.KeyData{} + s.KeyConfig[command.RESET] = keyinfo.KeyData{} s.sendUIBridgeUpdate() logger.Infof(logModule, "created default config") } diff --git a/config/service_test.go b/config/service_test.go index 1af02ad..5867100 100644 --- a/config/service_test.go +++ b/config/service_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/zellydev-games/opensplit/dispatcher" + "github.com/zellydev-games/opensplit/command" "github.com/zellydev-games/opensplit/keyinfo" ) @@ -62,11 +62,11 @@ func TestUpdateKeyBinding(t *testing.T) { ch := make(chan *Service, 1) s := &Service{ - KeyConfig: make(map[dispatcher.Command]keyinfo.KeyData), + KeyConfig: make(map[command.Command]keyinfo.KeyData), configUpdatedChannel: ch, } - cmd := dispatcher.SPLIT + cmd := command.SPLIT data := keyinfo.KeyData{ KeyCode: 32, LocaleName: "SPACE", @@ -107,12 +107,12 @@ func TestCreateDefaultConfig(t *testing.T) { } // Required commands should always be present - required := []dispatcher.Command{ - dispatcher.SPLIT, - dispatcher.UNDO, - dispatcher.SKIP, - dispatcher.PAUSE, - dispatcher.RESET, + required := []command.Command{ + command.SPLIT, + command.UNDO, + command.SKIP, + command.PAUSE, + command.RESET, } for _, cmd := range required { diff --git a/dispatcher/service.go b/dispatcher/service.go index cc65087..d86e7bc 100644 --- a/dispatcher/service.go +++ b/dispatcher/service.go @@ -4,6 +4,7 @@ import ( "sync" "github.com/wailsapp/wails/v2/pkg/runtime" + "github.com/zellydev-games/opensplit/command" "github.com/zellydev-games/opensplit/logger" ) @@ -19,27 +20,9 @@ type FolderProvider interface { OpenSkinsDir() } -// Command bytes are sent to the Service.Dispatch method receiver to indicate the state machine should take some action. -type Command byte - -const ( - QUIT Command = iota - NEW - LOAD - EDIT - CANCEL - SUBMIT - CLOSE - RESET - SAVE - SPLIT - UNDO - SKIP - PAUSE - TOGGLEGLOBAL - FOCUS - HELLO -) +type FileProvider interface { + LoadSplitFile() ([]byte, error) +} // DispatchReply is sent in response to Dispatch // @@ -50,7 +33,7 @@ type DispatchReply struct { } type DispatchReceiver interface { - ReceiveDispatch(Command, *string) (DispatchReply, error) + ReceiveDispatch(command.Command, *string) (DispatchReply, error) } type Service struct { @@ -58,24 +41,27 @@ type Service struct { receiver DispatchReceiver runtime RuntimeProvider folderProvider FolderProvider + fileProvider FileProvider } func NewService(receiver DispatchReceiver, runtime RuntimeProvider, folderProvider FolderProvider, + fileProvider FileProvider, ) *Service { return &Service{ runtime: runtime, receiver: receiver, folderProvider: folderProvider, + fileProvider: fileProvider, } } -func (s *Service) Dispatch(command Command, payload *string) (DispatchReply, error) { - logger.Debugf(logModule, "dispatching command: %v", command) +func (s *Service) Dispatch(cmd command.Command, payload *string) (DispatchReply, error) { + logger.Debugf(logModule, "dispatching cmd: %v", cmd) s.mu.Lock() defer s.mu.Unlock() - return s.receiver.ReceiveDispatch(command, payload) + return s.receiver.ReceiveDispatch(cmd, payload) } func (s *Service) OpenSplitFileFolder() { @@ -85,3 +71,12 @@ func (s *Service) OpenSplitFileFolder() { func (s *Service) OpenSkinsFolder() { s.folderProvider.OpenSkinsDir() } + +func (s *Service) ExportSplitFile() error { + _, err := s.fileProvider.LoadSplitFile() + if err != nil { + return err + } + + return nil +} diff --git a/dispatcher/service_test.go b/dispatcher/service_test.go index fc6d9e8..17fb4b9 100644 --- a/dispatcher/service_test.go +++ b/dispatcher/service_test.go @@ -1,10 +1,14 @@ package dispatcher -import "testing" +import ( + "testing" + + "github.com/zellydev-games/opensplit/command" +) type mockDispatchReceiver struct{} -func (r mockDispatchReceiver) ReceiveDispatch(Command, *string) (DispatchReply, error) { +func (r mockDispatchReceiver) ReceiveDispatch(command.Command, *string) (DispatchReply, error) { return DispatchReply{ Code: 69, Message: "Nice.", @@ -13,8 +17,8 @@ func (r mockDispatchReceiver) ReceiveDispatch(Command, *string) (DispatchReply, func TestDispatch(t *testing.T) { dr := mockDispatchReceiver{} - s := NewService(dr, nil, nil) - reply, _ := s.Dispatch(SPLIT, nil) + s := NewService(dr, nil, nil, nil) + reply, _ := s.Dispatch(command.SPLIT, nil) if reply.Code != 69 || reply.Message != "Nice." { t.Fatalf("Dispatch expected to return code 69 with message Nice. but got %v: %s", reply.Code, reply.Message) } diff --git a/frontend/src/components/SkinPicker.tsx b/frontend/src/components/SkinPicker.tsx deleted file mode 100644 index dabcd2b..0000000 --- a/frontend/src/components/SkinPicker.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { useEffect, useState } from "react"; - -import { setActiveSkin } from "../skinLoader"; - -export default function SkinPicker() { - const [active, setActive] = useState("default"); - const [available] = useState([]); - - useEffect(() => { - setTimeout(async () => { - //const skins = await GetAvailableSkins(); - //setAvailable(skins); - }, 1000); - }, []); - - return ( -
- {available && - available.map((name) => ( - - ))} - -
- ); -} diff --git a/frontend/wailsjs/go/dispatcher/Service.d.ts b/frontend/wailsjs/go/dispatcher/Service.d.ts index d162b79..2308a6a 100644 --- a/frontend/wailsjs/go/dispatcher/Service.d.ts +++ b/frontend/wailsjs/go/dispatcher/Service.d.ts @@ -1,8 +1,11 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT +import {command} from '../models'; import {dispatcher} from '../models'; -export function Dispatch(arg1:dispatcher.Command,arg2:any):Promise; +export function Dispatch(arg1:command.Command,arg2:any):Promise; + +export function ExportSplitFile():Promise; export function OpenSkinsFolder():Promise; diff --git a/frontend/wailsjs/go/dispatcher/Service.js b/frontend/wailsjs/go/dispatcher/Service.js index de62706..9e52eb3 100644 --- a/frontend/wailsjs/go/dispatcher/Service.js +++ b/frontend/wailsjs/go/dispatcher/Service.js @@ -6,6 +6,10 @@ export function Dispatch(arg1, arg2) { return window['go']['dispatcher']['Service']['Dispatch'](arg1, arg2); } +export function ExportSplitFile() { + return window['go']['dispatcher']['Service']['ExportSplitFile'](); +} + export function OpenSkinsFolder() { return window['go']['dispatcher']['Service']['OpenSkinsFolder'](); } diff --git a/opensplit.go b/opensplit.go index 42be385..32d513a 100644 --- a/opensplit.go +++ b/opensplit.go @@ -76,7 +76,7 @@ func main() { // Build dispatcher that can receive commands from frontend or backend and dispatch them to the state machine folderProvider := platform.NewFolderProvider(configService) - commandDispatcher := dispatcher.NewService(machine, runtimeProvider, folderProvider) + commandDispatcher := dispatcher.NewService(machine, runtimeProvider, folderProvider, jsonRepo) var hotkeyProvider statemachine.HotkeyProvider diff --git a/repo/adapters/session.go b/repo/adapters/session.go index 49e521c..cd1cbd5 100644 --- a/repo/adapters/session.go +++ b/repo/adapters/session.go @@ -28,3 +28,27 @@ func DomainToDTO(svc *session.Service) *dto.Session { Dirty: svc.Dirty(), } } + +func ExportSessionSplitfile(splitFile *session.SplitFile) { + sf := session.DeepCopySplitFile(splitFile) + sf.WindowY = 100 + sf.WindowX = 100 + sf.Attempts = 0 + sf.SOB = 0 + sf.Runs = []session.Run{} + sf.PB = nil + + for i := 0; i < len(sf.Segments); i++ { + clearSegmentRecursive(&sf.Segments[i]) + } +} + +func clearSegmentRecursive(segment *session.Segment) { + segment.PB = 0 + segment.Gold = 0 + segment.Average = 0 + + for i := 0; i < len(segment.Children); i++ { + clearSegmentRecursive(&segment.Children[i]) + } +} diff --git a/session/service.go b/session/service.go index e0043c0..1a259f3 100644 --- a/session/service.go +++ b/session/service.go @@ -280,7 +280,7 @@ func (s *Service) SplitFile() (SplitFile, bool) { if s.loadedSplitFile == nil { return sf, false } - return deepCopySplitFile(s.loadedSplitFile), true + return DeepCopySplitFile(s.loadedSplitFile), true } // Dirty returns the unsaved changes status of the session @@ -432,80 +432,3 @@ func (s *Service) sendUpdate() { default: } } - -func deepCopySplitFile(inFile *SplitFile) SplitFile { - segments := deepCopySegments(inFile.Segments) - runs := deepCopyRuns(inFile.Runs) - - var pbRun *Run - if inFile.PB != nil { - pbCopy := deepCopyRun(*inFile.PB) - pbRun = &pbCopy - } - - return SplitFile{ - ID: inFile.ID, - GameName: inFile.GameName, - GameCategory: inFile.GameCategory, - Version: inFile.Version, - Attempts: inFile.Attempts, - Offset: inFile.Offset, - Segments: segments, - WindowX: inFile.WindowX, - WindowY: inFile.WindowY, - WindowHeight: inFile.WindowHeight, - WindowWidth: inFile.WindowWidth, - SOB: inFile.SOB, - Runs: runs, - PB: pbRun, - } -} - -func deepCopyRuns(inRuns []Run) []Run { - runs := make([]Run, len(inRuns)) - for i := range inRuns { - runs[i] = deepCopyRun(inRuns[i]) - } - return runs -} - -func deepCopyRun(run Run) Run { - segments := deepCopySegments(run.LeafSegments) - splits := deepCopySplits(run.Splits) - - return Run{ - ID: run.ID, - SplitFileVersion: run.SplitFileVersion, - TotalTime: run.TotalTime, - Splits: splits, - Completed: run.Completed, - LeafSegments: segments, - } -} - -func deepCopySplits(inSplits map[uuid.UUID]Split) map[uuid.UUID]Split { - splits := map[uuid.UUID]Split{} - for segmentID, split := range inSplits { - splits[segmentID] = Split{ - SplitSegmentID: split.SplitSegmentID, - CurrentCumulative: split.CurrentCumulative, - CurrentDuration: split.CurrentDuration, - } - } - return splits -} - -func deepCopySegments(list []Segment) []Segment { - out := make([]Segment, len(list)) - for i, s := range list { - out[i] = Segment{ - ID: s.ID, - Name: s.Name, - Gold: s.Gold, - Average: s.Average, - PB: s.PB, - Children: deepCopySegments(s.Children), - } - } - return out -} diff --git a/session/splitfile.go b/session/splitfile.go index f53923c..91cb77d 100644 --- a/session/splitfile.go +++ b/session/splitfile.go @@ -119,6 +119,34 @@ func (s *SplitFile) perSegmentAggregates(runs []Run) (golds map[uuid.UUID]time.D return golds, sums, counts } +func DeepCopySplitFile(inFile *SplitFile) SplitFile { + segments := deepCopySegments(inFile.Segments) + runs := deepCopyRuns(inFile.Runs) + + var pbRun *Run + if inFile.PB != nil { + pbCopy := deepCopyRun(*inFile.PB) + pbRun = &pbCopy + } + + return SplitFile{ + ID: inFile.ID, + GameName: inFile.GameName, + GameCategory: inFile.GameCategory, + Version: inFile.Version, + Attempts: inFile.Attempts, + Offset: inFile.Offset, + Segments: segments, + WindowX: inFile.WindowX, + WindowY: inFile.WindowY, + WindowHeight: inFile.WindowHeight, + WindowWidth: inFile.WindowWidth, + SOB: inFile.SOB, + Runs: runs, + PB: pbRun, + } +} + func getPB(runs []Run) (*Run, time.Duration, error) { if len(runs) == 0 { return nil, 0, errors.New("no runs found") @@ -153,3 +181,52 @@ func getLeafSegments(segments []Segment, out []*Segment) []*Segment { } return out } + +func deepCopyRuns(inRuns []Run) []Run { + runs := make([]Run, len(inRuns)) + for i := range inRuns { + runs[i] = deepCopyRun(inRuns[i]) + } + return runs +} + +func deepCopyRun(run Run) Run { + segments := deepCopySegments(run.LeafSegments) + splits := deepCopySplits(run.Splits) + + return Run{ + ID: run.ID, + SplitFileVersion: run.SplitFileVersion, + TotalTime: run.TotalTime, + Splits: splits, + Completed: run.Completed, + LeafSegments: segments, + } +} + +func deepCopySplits(inSplits map[uuid.UUID]Split) map[uuid.UUID]Split { + splits := map[uuid.UUID]Split{} + for segmentID, split := range inSplits { + splits[segmentID] = Split{ + SplitSegmentID: split.SplitSegmentID, + CurrentCumulative: split.CurrentCumulative, + CurrentDuration: split.CurrentDuration, + } + } + return splits +} + +func deepCopySegments(list []Segment) []Segment { + out := make([]Segment, len(list)) + for i, s := range list { + out[i] = Segment{ + ID: s.ID, + Name: s.Name, + Gold: s.Gold, + Average: s.Average, + PB: s.PB, + Children: deepCopySegments(s.Children), + } + } + return out +} diff --git a/statemachine/config.go b/statemachine/config.go index cd86a2e..92d2d38 100644 --- a/statemachine/config.go +++ b/statemachine/config.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/zellydev-games/opensplit/bridge" + "github.com/zellydev-games/opensplit/command" "github.com/zellydev-games/opensplit/dispatcher" "github.com/zellydev-games/opensplit/keyinfo" "github.com/zellydev-games/opensplit/logger" @@ -15,7 +16,7 @@ const RecordingArmed = 10 type Config struct { mu sync.Mutex - listeningFor dispatcher.Command + listeningFor command.Command recordingArmed bool previousState StateID } @@ -38,26 +39,26 @@ func (c *Config) OnExit() error { return nil } -func (c *Config) Receive(command dispatcher.Command, payload *string) (dispatcher.DispatchReply, error) { +func (c *Config) Receive(cmd command.Command, _ *string) (dispatcher.DispatchReply, error) { c.mu.Lock() defer c.mu.Unlock() - switch command { - case dispatcher.SPLIT: + switch cmd { + case command.SPLIT: fallthrough - case dispatcher.UNDO: + case command.UNDO: fallthrough - case dispatcher.SKIP: + case command.SKIP: fallthrough - case dispatcher.PAUSE: + case command.PAUSE: fallthrough - case dispatcher.RESET: + case command.RESET: c.recordingArmed = true - c.listeningFor = command - logger.Infof(logModule, "recording armed for command: %d", c.listeningFor) + c.listeningFor = cmd + logger.Infof(logModule, "recording armed for cmd: %d", c.listeningFor) err := machine.hotkeyProvider.StartHook(func(data keyinfo.KeyData) { c.handleHotkey(data) c.recordingArmed = false - logger.Infof(logModule, "updated command %v with hotkey %s (%d)", + logger.Infof(logModule, "updated cmd %v with hotkey %s (%d)", c.listeningFor, data.LocaleName, data.KeyCode) err := machine.hotkeyProvider.Unhook() if err != nil { @@ -70,10 +71,10 @@ func (c *Config) Receive(command dispatcher.Command, payload *string) (dispatche return dispatcher.DispatchReply{Code: 6}, err } return dispatcher.DispatchReply{Code: RecordingArmed}, nil - case dispatcher.CANCEL: + case command.CANCEL: machine.changeState(c.previousState) return dispatcher.DispatchReply{}, nil - case dispatcher.SUBMIT: + case command.SUBMIT: err := machine.repoService.SaveConfig(machine.configService) if err != nil { message := fmt.Sprintf("error saving config to repo %s", err) @@ -83,7 +84,7 @@ func (c *Config) Receive(command dispatcher.Command, payload *string) (dispatche machine.changeState(c.previousState) return dispatcher.DispatchReply{}, nil default: - message := fmt.Sprintf("unknown command sent to config service: %v", command) + message := fmt.Sprintf("unknown cmd sent to config service: %v", cmd) return dispatcher.DispatchReply{Code: 5, Message: message}, errors.New(message) } } diff --git a/statemachine/editing.go b/statemachine/editing.go index fec5688..940577a 100644 --- a/statemachine/editing.go +++ b/statemachine/editing.go @@ -4,6 +4,7 @@ import ( "errors" "github.com/zellydev-games/opensplit/bridge" + "github.com/zellydev-games/opensplit/command" "github.com/zellydev-games/opensplit/dispatcher" "github.com/zellydev-games/opensplit/repo/adapters" ) @@ -34,11 +35,11 @@ func (e *Editing) OnEnter() error { } func (e *Editing) OnExit() error { return nil } -func (e *Editing) Receive(command dispatcher.Command, payload *string) (dispatcher.DispatchReply, error) { - switch command { - case dispatcher.CANCEL: +func (e *Editing) Receive(c command.Command, payload *string) (dispatcher.DispatchReply, error) { + switch c { + case command.CANCEL: machine.changeState(RUNNING) - case dispatcher.SUBMIT: + case command.SUBMIT: if payload == nil { return dispatcher.DispatchReply{ Code: 1, diff --git a/statemachine/newFile.go b/statemachine/newFile.go index 15dd4ea..71d02f0 100644 --- a/statemachine/newFile.go +++ b/statemachine/newFile.go @@ -2,6 +2,7 @@ package statemachine import ( "github.com/zellydev-games/opensplit/bridge" + "github.com/zellydev-games/opensplit/command" "github.com/zellydev-games/opensplit/dispatcher" "github.com/zellydev-games/opensplit/logger" "github.com/zellydev-games/opensplit/repo/adapters" @@ -32,11 +33,11 @@ func (n *NewFile) OnEnter() error { return nil } func (n *NewFile) OnExit() error { return nil } -func (n *NewFile) Receive(command dispatcher.Command, payload *string) (dispatcher.DispatchReply, error) { - switch command { - case dispatcher.CANCEL: +func (n *NewFile) Receive(c command.Command, payload *string) (dispatcher.DispatchReply, error) { + switch c { + case command.CANCEL: machine.changeState(WELCOME) - case dispatcher.SUBMIT: + case command.SUBMIT: if payload == nil { return dispatcher.DispatchReply{ Code: 1, diff --git a/statemachine/running.go b/statemachine/running.go index 7663cab..87bf396 100644 --- a/statemachine/running.go +++ b/statemachine/running.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/zellydev-games/opensplit/bridge" + "github.com/zellydev-games/opensplit/command" "github.com/zellydev-games/opensplit/dispatcher" "github.com/zellydev-games/opensplit/keyinfo" "github.com/zellydev-games/opensplit/logger" @@ -26,7 +27,7 @@ func (r *Running) OnEnter() error { return } - for command, keyData := range machine.configService.KeyConfig { + for c, keyData := range machine.configService.KeyConfig { if keyData.KeyCode != data.KeyCode { continue } @@ -54,10 +55,10 @@ func (r *Running) OnEnter() error { if !match { continue } - _, _ = machine.ReceiveDispatch(command, nil) + _, _ = machine.ReceiveDispatch(c, nil) return } else { - _, _ = machine.ReceiveDispatch(command, nil) + _, _ = machine.ReceiveDispatch(c, nil) return } } @@ -89,10 +90,10 @@ func (r *Running) OnExit() error { return nil } -func (r *Running) Receive(command dispatcher.Command, _ *string) (dispatcher.DispatchReply, error) { - switch command { - case dispatcher.CLOSE: - logger.Debug(logModule, "Running received CLOSE command") +func (r *Running) Receive(c command.Command, _ *string) (dispatcher.DispatchReply, error) { + switch c { + case command.CLOSE: + logger.Debug(logModule, "Running received CLOSE c") err := machine.promptDirtySave() if err != nil { return dispatcher.DispatchReply{}, err @@ -100,37 +101,37 @@ func (r *Running) Receive(command dispatcher.Command, _ *string) (dispatcher.Dis machine.sessionService.CloseRun() machine.repoService.Close() machine.changeState(WELCOME, nil) - case dispatcher.EDIT: - logger.Debug(logModule, "Running received EDIT command") + case command.EDIT: + logger.Debug(logModule, "Running received EDIT c") if _, ok := machine.sessionService.Run(); ok { return dispatcher.DispatchReply{Code: 1, Message: "can't edit splitfile mid run"}, nil } machine.changeState(EDITING, nil) - case dispatcher.SAVE: - logger.Debug(logModule, "Running received SAVE command") + case command.SAVE: + logger.Debug(logModule, "Running received SAVE c") err := machine.saveSplitFile() if err != nil { msg := fmt.Sprintf("failed to save split file to session: %s", err) logger.Error(logModule, msg) return dispatcher.DispatchReply{Code: 2, Message: msg}, err } - case dispatcher.SPLIT: - logger.Debug(logModule, "Running received SPLIT command") + case command.SPLIT: + logger.Debug(logModule, "Running received SPLIT c") machine.sessionService.Split() - case dispatcher.UNDO: + case command.UNDO: machine.sessionService.Undo() - case dispatcher.SKIP: + case command.SKIP: machine.sessionService.Skip() - case dispatcher.PAUSE: + case command.PAUSE: machine.sessionService.Pause() - case dispatcher.RESET: + case command.RESET: _ = machine.promptPartialRun() // note: promptPartialRun only adds the partial run to the session's loadedSplitFile's Runs slice. // Nothing has been saved to disk at this point, so keep the file dirty if needs be. machine.sessionService.Reset() default: - logger.Warnf(logModule, "unhandled default case in Running: %d", command) + logger.Warnf(logModule, "unhandled default case in Running: %d", c) } return dispatcher.DispatchReply{}, nil diff --git a/statemachine/service.go b/statemachine/service.go index 665b208..1458116 100644 --- a/statemachine/service.go +++ b/statemachine/service.go @@ -7,6 +7,7 @@ import ( "sync" "github.com/wailsapp/wails/v2/pkg/runtime" + "github.com/zellydev-games/opensplit/command" "github.com/zellydev-games/opensplit/config" "github.com/zellydev-games/opensplit/dispatcher" "github.com/zellydev-games/opensplit/keyinfo" @@ -55,7 +56,7 @@ type HotkeyProvider interface { type state interface { OnEnter() error OnExit() error - Receive(command dispatcher.Command, payload *string) (dispatcher.DispatchReply, error) + Receive(c command.Command, payload *string) (dispatcher.DispatchReply, error) String() string ID() StateID } @@ -99,14 +100,14 @@ func (s *Service) AttachHotkeyProvider(provider HotkeyProvider) { } // ReceiveDispatch allows external facing code to send Command bytes to the state machine -func (s *Service) ReceiveDispatch(command dispatcher.Command, payload *string) (dispatcher.DispatchReply, error) { +func (s *Service) ReceiveDispatch(c command.Command, payload *string) (dispatcher.DispatchReply, error) { if s.currentState == nil { - logger.Error(logModule, "command sent to state machine without a loaded state") - return dispatcher.DispatchReply{}, errors.New("command sent to state machine without a loaded state") + logger.Error(logModule, "c sent to state machine without a loaded state") + return dispatcher.DispatchReply{}, errors.New("c sent to state machine without a loaded state") } - if command == dispatcher.QUIT { - logger.Debug(logModule, "QUIT command dispatched from front end") + if c == command.QUIT { + logger.Debug(logModule, "QUIT c dispatched from front end") _ = s.promptDirtySave() if s.unsubscribeFromWindowDimensionChanges != nil { s.unsubscribeFromWindowDimensionChanges() @@ -115,15 +116,15 @@ func (s *Service) ReceiveDispatch(command dispatcher.Command, payload *string) ( return dispatcher.DispatchReply{}, nil } - if command == dispatcher.HELLO { + if c == command.HELLO { return dispatcher.DispatchReply{ Code: 0, Message: "HELLO", }, nil } - if command == dispatcher.TOGGLEGLOBAL { - logger.Debug(logModule, "TOGGLEGLOBAL command dispatched from frontend") + if c == command.TOGGLEGLOBAL { + logger.Debug(logModule, "TOGGLEGLOBAL c dispatched from frontend") s.configService.GlobalHotkeysActive = !s.configService.GlobalHotkeysActive err := machine.repoService.SaveConfig(machine.configService) if err != nil { @@ -136,7 +137,7 @@ func (s *Service) ReceiveDispatch(command dispatcher.Command, payload *string) ( }, nil } - if command == dispatcher.FOCUS { + if c == command.FOCUS { if payload == nil { return dispatcher.DispatchReply{Code: 1, Message: "focus requires payload of \"true\" or \"false\""}, nil } @@ -145,8 +146,8 @@ func (s *Service) ReceiveDispatch(command dispatcher.Command, payload *string) ( return dispatcher.DispatchReply{}, nil } - logger.Debugf(logModule, "command %d dispatched to state %s", command, s.currentState.String()) - return s.currentState.Receive(command, payload) + logger.Debugf(logModule, "c %d dispatched to state %s", c, s.currentState.String()) + return s.currentState.Receive(c, payload) } // changeState provides a structured way to change the current state, calling appropriate lifecycle methods along the way diff --git a/statemachine/welcome.go b/statemachine/welcome.go index d920321..f1bd229 100644 --- a/statemachine/welcome.go +++ b/statemachine/welcome.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/zellydev-games/opensplit/bridge" + "github.com/zellydev-games/opensplit/command" "github.com/zellydev-games/opensplit/dispatcher" "github.com/zellydev-games/opensplit/logger" ) @@ -33,10 +34,10 @@ func (w *Welcome) OnEnter() error { return nil } func (w *Welcome) OnExit() error { return nil } -func (w *Welcome) Receive(command dispatcher.Command, _ *string) (dispatcher.DispatchReply, error) { - switch command { - case dispatcher.LOAD: - logger.Debug(logModule, "Welcome received command LOAD") +func (w *Welcome) Receive(c command.Command, _ *string) (dispatcher.DispatchReply, error) { + switch c { + case command.LOAD: + logger.Debug(logModule, "Welcome received c LOAD") sf, err := machine.repoService.LoadSplitFile() if err != nil { return dispatcher.DispatchReply{Code: 1, Message: "failed to load dto: " + err.Error()}, err @@ -44,15 +45,15 @@ func (w *Welcome) Receive(command dispatcher.Command, _ *string) (dispatcher.Dis machine.sessionService.SetLoadedSplitFile(sf) machine.changeState(RUNNING) return dispatcher.DispatchReply{}, nil - case dispatcher.NEW: - logger.Debug(logModule, "Welcome received command NEW") + case command.NEW: + logger.Debug(logModule, "Welcome received c NEW") machine.changeState(NEWFILE) return dispatcher.DispatchReply{}, nil - case dispatcher.EDIT: - logger.Debug(logModule, "Welcome received command EDIT") + case command.EDIT: + logger.Debug(logModule, "Welcome received c EDIT") machine.changeState(CONFIG) return dispatcher.DispatchReply{}, nil default: - return dispatcher.DispatchReply{}, fmt.Errorf("invalid command %d for state Welcome", command) + return dispatcher.DispatchReply{}, fmt.Errorf("invalid c %d for state Welcome", c) } } From 8bcc9865daf344096f94c52c6cacabc9be319788 Mon Sep 17 00:00:00 2001 From: Jeremy Smitherman Date: Wed, 11 Feb 2026 16:29:56 -0600 Subject: [PATCH 2/4] Added default skin file --- Taskfile.yml | 24 +++++++++++- dispatcher/service.go | 35 ++++++++++++++---- frontend/src/components/Config.tsx | 1 + .../src/components/splitter/SegmentList.tsx | 3 ++ frontend/wailsjs/go/dispatcher/Service.d.ts | 2 +- frontend/wailsjs/go/dispatcher/Service.js | 4 +- opensplit.go | 2 +- repo/adapters/session.go | 11 +++++- repo/service.go | 7 ++-- skin/default-skin.zip | Bin 2178 -> 1933 bytes skin/service.go | 4 +- statemachine/welcome.go | 7 +++- 12 files changed, 79 insertions(+), 21 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 64da848..d6c24df 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -122,7 +122,7 @@ tasks: echo 1 # If env WAILS_TAGS is set (e.g. by GHA), use it. - # Otherwise compute tags dynamically for local dev. + # Otherwise, compute tags dynamically for local dev. WAILS_TAGS: sh: | set -euo pipefail @@ -191,3 +191,25 @@ tasks: platforms: [windows] cmds: - wails build -clean + + package-default-skin: + desc: Zip folder into ./skin/default-skin.zip + vars: + SRC_DIR: '{{.SRC_DIR}}' + ZIP_PATH: "./skin/default-skin.zip" + + cmds: + - cmd: | + set -e + mkdir -p "$(dirname "{{.ZIP_PATH}}")" + parent="$(cd "$(dirname "{{.SRC_DIR}}")" && pwd)" + base="$(basename "{{.SRC_DIR}}")" + (cd "$parent" && zip -r "$OLDPWD/{{.ZIP_PATH}}" "$base") + platforms: [ linux, darwin ] + + - cmd: > + powershell -NoProfile -Command + "New-Item -ItemType Directory -Force -Path (Split-Path -Parent '{{.ZIP_PATH}}') | Out-Null; + if (Test-Path -LiteralPath '{{.ZIP_PATH}}') { Remove-Item '{{.ZIP_PATH}}' -Force }; + Compress-Archive -LiteralPath '{{.SRC_DIR}}' -DestinationPath '{{.ZIP_PATH}}' -Force" + platforms: [ windows ] diff --git a/dispatcher/service.go b/dispatcher/service.go index d86e7bc..6165b91 100644 --- a/dispatcher/service.go +++ b/dispatcher/service.go @@ -5,7 +5,9 @@ import ( "github.com/wailsapp/wails/v2/pkg/runtime" "github.com/zellydev-games/opensplit/command" + "github.com/zellydev-games/opensplit/dto" "github.com/zellydev-games/opensplit/logger" + "github.com/zellydev-games/opensplit/repo/adapters" ) const logModule = "dispatcher" @@ -20,8 +22,9 @@ type FolderProvider interface { OpenSkinsDir() } -type FileProvider interface { - LoadSplitFile() ([]byte, error) +type RepoProvider interface { + LoadSplitFile() (dto.SplitFile, error) + SaveSplitFile(dto.SplitFile) error } // DispatchReply is sent in response to Dispatch @@ -41,19 +44,19 @@ type Service struct { receiver DispatchReceiver runtime RuntimeProvider folderProvider FolderProvider - fileProvider FileProvider + repo RepoProvider } func NewService(receiver DispatchReceiver, runtime RuntimeProvider, folderProvider FolderProvider, - fileProvider FileProvider, + repo RepoProvider, ) *Service { return &Service{ runtime: runtime, receiver: receiver, folderProvider: folderProvider, - fileProvider: fileProvider, + repo: repo, } } @@ -72,11 +75,29 @@ func (s *Service) OpenSkinsFolder() { s.folderProvider.OpenSkinsDir() } -func (s *Service) ExportSplitFile() error { - _, err := s.fileProvider.LoadSplitFile() +func (s *Service) ExportSplitFile(platform string) error { + sf, err := s.repo.LoadSplitFile() if err != nil { return err } + _, err = adapters.CleanSplitFile(sf) + if err != nil { + return err + } + + //fileName, err := s.runtime.OpenFileDialog(runtime.OpenDialogOptions{ + // DefaultFilename: fmt.Sprintf("%s-%s-%s.osf", file.GameName, file.GameCategory, platform), + // Title: "Save Exported File", + // Filters: []runtime.FileFilter{{ + // DisplayName: "OpenSplit File", + // Pattern: "*.osf", + // }}, + // CanCreateDirectories: true, + //}) + //if err != nil { + // return err + //} + return nil } diff --git a/frontend/src/components/Config.tsx b/frontend/src/components/Config.tsx index c9f6de4..24156e3 100644 --- a/frontend/src/components/Config.tsx +++ b/frontend/src/components/Config.tsx @@ -27,6 +27,7 @@ export default function Config({ configPayload }: ConfigParams) { useEffect(() => { (async () => { const as = await GetAvailableSkins(); + console.log(as); setAvailableSkins(as); })(); }, []); diff --git a/frontend/src/components/splitter/SegmentList.tsx b/frontend/src/components/splitter/SegmentList.tsx index 0bd2178..f442ffb 100644 --- a/frontend/src/components/splitter/SegmentList.tsx +++ b/frontend/src/components/splitter/SegmentList.tsx @@ -384,6 +384,9 @@ export default function SegmentList({ sessionPayload, comparison }: SplitListPar

{sessionPayload.loaded_split_file?.game_category}

+
+ {sessionPayload.loaded_split_file?.attempts} +
diff --git a/frontend/wailsjs/go/dispatcher/Service.d.ts b/frontend/wailsjs/go/dispatcher/Service.d.ts index 2308a6a..275e815 100644 --- a/frontend/wailsjs/go/dispatcher/Service.d.ts +++ b/frontend/wailsjs/go/dispatcher/Service.d.ts @@ -5,7 +5,7 @@ import {dispatcher} from '../models'; export function Dispatch(arg1:command.Command,arg2:any):Promise; -export function ExportSplitFile():Promise; +export function ExportSplitFile(arg1:string):Promise; export function OpenSkinsFolder():Promise; diff --git a/frontend/wailsjs/go/dispatcher/Service.js b/frontend/wailsjs/go/dispatcher/Service.js index 9e52eb3..f2aad75 100644 --- a/frontend/wailsjs/go/dispatcher/Service.js +++ b/frontend/wailsjs/go/dispatcher/Service.js @@ -6,8 +6,8 @@ export function Dispatch(arg1, arg2) { return window['go']['dispatcher']['Service']['Dispatch'](arg1, arg2); } -export function ExportSplitFile() { - return window['go']['dispatcher']['Service']['ExportSplitFile'](); +export function ExportSplitFile(arg1) { + return window['go']['dispatcher']['Service']['ExportSplitFile'](arg1); } export function OpenSkinsFolder() { diff --git a/opensplit.go b/opensplit.go index 32d513a..c5f6f2a 100644 --- a/opensplit.go +++ b/opensplit.go @@ -76,7 +76,7 @@ func main() { // Build dispatcher that can receive commands from frontend or backend and dispatch them to the state machine folderProvider := platform.NewFolderProvider(configService) - commandDispatcher := dispatcher.NewService(machine, runtimeProvider, folderProvider, jsonRepo) + commandDispatcher := dispatcher.NewService(machine, runtimeProvider, folderProvider, repoService) var hotkeyProvider statemachine.HotkeyProvider diff --git a/repo/adapters/session.go b/repo/adapters/session.go index cd1cbd5..a27c352 100644 --- a/repo/adapters/session.go +++ b/repo/adapters/session.go @@ -29,8 +29,13 @@ func DomainToDTO(svc *session.Service) *dto.Session { } } -func ExportSessionSplitfile(splitFile *session.SplitFile) { - sf := session.DeepCopySplitFile(splitFile) +func CleanSplitFile(dtoSplitFile dto.SplitFile) (dto.SplitFile, error) { + splitFile, err := DTOSplitFileToDomain(dtoSplitFile) + if err != nil { + return dto.SplitFile{}, err + } + + sf := session.DeepCopySplitFile(&splitFile) sf.WindowY = 100 sf.WindowX = 100 sf.Attempts = 0 @@ -41,6 +46,8 @@ func ExportSessionSplitfile(splitFile *session.SplitFile) { for i := 0; i < len(sf.Segments); i++ { clearSegmentRecursive(&sf.Segments[i]) } + + return DomainSplitFileToDTO(sf), nil } func clearSegmentRecursive(segment *session.Segment) { diff --git a/repo/service.go b/repo/service.go index 7efa9c2..72fb8da 100644 --- a/repo/service.go +++ b/repo/service.go @@ -8,7 +8,6 @@ import ( "github.com/zellydev-games/opensplit/dto" "github.com/zellydev-games/opensplit/logger" "github.com/zellydev-games/opensplit/repo/adapters" - "github.com/zellydev-games/opensplit/session" ) const logModule = "repo" @@ -38,18 +37,18 @@ func NewService(repository Repository) *Service { } // LoadSplitFile reads splitfile bytes from a repo and returns it as a session.SplitFile -func (s *Service) LoadSplitFile() (session.SplitFile, error) { +func (s *Service) LoadSplitFile() (dto.SplitFile, error) { logger.Debug(logModule, "loading split file") s.splitFileLock.RLock() splitFile, err := s.repository.LoadSplitFile() if err != nil { s.splitFileLock.RUnlock() - return session.SplitFile{}, err + return dto.SplitFile{}, err } s.splitFileLock.RUnlock() splitFileDTO, _ := adapters.JSONSplitFileToDTO(string(splitFile)) logger.Infof(logModule, "loaded split file: %s-%s", splitFileDTO.GameName, splitFileDTO.GameCategory) - return adapters.DTOSplitFileToDomain(splitFileDTO) + return splitFileDTO, nil } // SaveSplitFileWindowDimensions loads the active filename in the repository service, diff --git a/skin/default-skin.zip b/skin/default-skin.zip index 90c582c49fc12e02e9f0661cbb401c70c7b790ce..5b10854ce1c1e0d9f8118be54d66b86eae05c73d 100644 GIT binary patch delta 1289 zcmZn??B&-F@MdNaVPIh3VEEJI9m98BzStDVO95g*5S^NqSejE3lbM&2TA`O*Ts+a; zydGJD|GfJ%RDcR~fmjqtLw;FmQBh_}YB5Mt@d?j0KDt3C^u2YpPWgE1u6y=0h=FPK zqCbCxG^CxbeJZ-j#DH)|OtW{)y1p&P+L;&_95@*mK(2$iqqra^v%aJxwFqSF(Xg|5 zw+(pqeb;2+VV;)cwOLfU?TL}jZXSuU$oPhBDefIQQj;cK(f;*r@l*TCvP)k~z9)VD zuKaW2%W&SQTh6?lTKMK=+r`hvrbbozUwXE?u0=^PBzs!s&mAw1e!V`cR)Tv+K-jXl zg^`<;Z0zP? zt2vfV@1NHkRv91K>nbQ_w?Zs<3xCMYo`;J*-dSm1B(-cC&#E1kTYs+1J+{UdMZ5JtS<`Yn=ZWOQ5JOQ{R-XH zS8qGcSRi`oTBqrhz7<#Y#`?W`d&$%H^5W_bB?}c6{FKYoo~`%kpn_DT#;+dd9lQr0 zwC4Q~>T_u1F_QJXlDt$PczoXu=9&Q06R zv;O|lZvw9Mv*%q;IOdvbzIQ|8@vmL!0UEJqf4aOnS{=6FU_rg!;af?8O`&{eMQ>cX zSHbdut7YPIiDk@>SIi7cO`hZMpHpOMx!@|Hxm@X-LhZjEq<)EReLAaFYq9q=zaRT1 zZ!Mh6{C0a}nfB*~O1sT7WbN&a3+_(+Bf% z)6CsL^YZ^OA(AgB8>O(H-SQonZ2vN%W}}kKTu9QLEWq+p2RR?b&n)E31FC+?%)lUk zl;Fz}i@;fEb1Z8CV?8622sk@n&#XXgNC1{);8{1o8&x}c)A3fj@`e(5q^uvNNz?+o~q=FR) L9f84Z2Ic_(E63(% delta 1536 zcmeC>ZxVD5@MdNaVc-D5{0VL`3`l^JL4hGNFD11?FS)q5w1S&~kp(0RCPG7a8Q6mx z+cGWUyE2Wc+A=5Vo7b~*@LiWLHU+9l0b+!~OVJGG1sa@RmReMlnUY$J%kXVb!?S>N z@d?j0KDt3C^u2YpPWgE1u6y=0h=FPKqCbCx6r>NWe)@D3E6_Ti{r?JdR;mE))DYmXxFx;j;c#co*1jJwU%54e{;2Y#^}rdw7JC_C?WmViN?i^gD_i zonmg-8+6XSvMSkq+i$_J`rr3wTKFbC-g4VEx5)F^>HK;3&zN)anjd-Bvgt{t+lR{Z zY|S~&+28Nm`798eC90`nzm~oD_jV=uWaiXEAMS9j4ST}y%y#L%1tQma*ZrIk%;~y4 zW=iySrPJsB*l8*j4mm)lnq@ZrRw+ zB5#$;&N=UhvdlUfKW|#5@?LF~<-GRuPR{V*P*Ur=-|%>;nQsGwwD!b@KCf!b1#GM) zC4VaBy!Dg0z|=r3#MAigoOOEYWpz^+pLo{iPY&Uk>3pJh%8|~=Ybp%uuCJ-AHvR1- z?ZV~c+Fa*7@!LiAJg$}fA`7d(xd~pY?F@W`m&$I8R=$$wyQZ1#`SFSG z9(CxQIB{$Ba`jkczD*fwRS$KAGhMeWPRQ%tqBFgGvEk*m!pm|x3%6-3{rCFU!*e=1 z(<`cX@Yl~YyR)oO8))px1Rl+ z({qye#=3K5pAX%7Z&&|#0;|ON?Sk@a4ce;CA1xFA_nCbRPg$XClP-->vqC>JLCPUpCJDp2^&m$f$M z|1k!5vvVj`F&3mSF)+N~K+Zxx(2_7HZ~ZSPB?{`7VBDA!i=Y)ThiKlNp&r zm=Q$^a+$!%AOW-=0=6}RXxs%1D+42_fI&76RFEKNabB=-^)T~#fDHWQ3eZeYxq@sa zsBl0Ia~>2Yasxeux2OP`SPv>Ikc|XI7IIjl`uZd=jL@SF=x}Jv0nG!&9I|ns2tp2E i4ip!Pvsy6VjxV5bp!f>#W@Q6~5-Si+1bXQ+W3u diff --git a/skin/service.go b/skin/service.go index cc1c271..9efae3a 100644 --- a/skin/service.go +++ b/skin/service.go @@ -80,14 +80,14 @@ func (s *Service) Startup() error { return errors.New(msg) } - target := filepath.Join(s.skinDir, "default") + target := s.skinDir if !strings.Contains(target, "OpenSplit") { msg := fmt.Sprintf("refusing to delete outside OpenSplit directory: %s", target) logger.Error(logModule, msg) return errors.New(msg) } - if err := os.RemoveAll(target); err != nil { + if err := os.RemoveAll(filepath.Join(target, "default")); err != nil { return err } diff --git a/statemachine/welcome.go b/statemachine/welcome.go index f1bd229..443eb19 100644 --- a/statemachine/welcome.go +++ b/statemachine/welcome.go @@ -7,6 +7,7 @@ import ( "github.com/zellydev-games/opensplit/command" "github.com/zellydev-games/opensplit/dispatcher" "github.com/zellydev-games/opensplit/logger" + "github.com/zellydev-games/opensplit/repo/adapters" ) // Welcome greets the user by indicating the frontend should display the Welcome screen @@ -42,7 +43,11 @@ func (w *Welcome) Receive(c command.Command, _ *string) (dispatcher.DispatchRepl if err != nil { return dispatcher.DispatchReply{Code: 1, Message: "failed to load dto: " + err.Error()}, err } - machine.sessionService.SetLoadedSplitFile(sf) + domainSF, err := adapters.DTOSplitFileToDomain(sf) + if err != nil { + return dispatcher.DispatchReply{Code: 2, Message: "failed to convert dto: " + err.Error()}, err + } + machine.sessionService.SetLoadedSplitFile(domainSF) machine.changeState(RUNNING) return dispatcher.DispatchReply{}, nil case command.NEW: From f99a32f4581ea4625aba05216b982d02de351dd3 Mon Sep 17 00:00:00 2001 From: Jeremy Smitherman Date: Wed, 11 Feb 2026 18:08:28 -0600 Subject: [PATCH 3/4] Finished exporter, added attempt counter to UI --- .osf | 1 + dispatcher/service.go | 27 +------ dto/splitfile.go | 1 + frontend/src/components/Config.tsx | 2 +- .../src/components/editor/SplitEditor.tsx | 76 ++++++++++++++++++- frontend/src/models/splitFilePayload.ts | 1 + repo/adapters/splitfile.go | 2 + repo/jsonfile.go | 34 ++++++--- repo/jsonfile_test.go | 2 +- repo/service.go | 28 ++++++- session/splitfile.go | 2 + 11 files changed, 135 insertions(+), 41 deletions(-) create mode 100644 .osf diff --git a/.osf b/.osf new file mode 100644 index 0000000..ee657c8 --- /dev/null +++ b/.osf @@ -0,0 +1 @@ +{"id":"4d8e6d26-8cc4-4916-a349-53c5fc87a4c3","version":0,"attempts":21,"game_name":"Final Fantasy VI World's Collide","game_category":"Ultros League","window_x":1539,"window_y":586,"window_height":388,"window_width":416,"runs":[{"id":"d7f6c8dd-0c32-48a8-bb0d-6d31c30059ee","split_file_version":0,"total_time":6844552,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3863072,"current_duration":3863072},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":6844552,"current_duration":2981480}},"leaf_segments":null,"completed":false},{"id":"434f571b-802d-4ec9-8e81-8da7d99a8769","split_file_version":0,"total_time":0,"splits":{},"leaf_segments":null,"completed":false},{"id":"e8ab994e-965a-4d1c-8f63-680b88f58488","split_file_version":0,"total_time":5151654,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3625974,"current_duration":3625974},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":5151654,"current_duration":1525680}},"leaf_segments":null,"completed":false},{"id":"da1ed76e-7104-4d19-9fa6-e3ab01b5890b","split_file_version":0,"total_time":5884904,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":4204444,"current_duration":4204444},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":5884904,"current_duration":1680460}},"leaf_segments":null,"completed":false},{"id":"1e2317eb-e693-4102-8901-411f8048206e","split_file_version":0,"total_time":5356052,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3810011,"current_duration":3810011},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":5356052,"current_duration":1546041}},"leaf_segments":null,"completed":false},{"id":"25fe2c27-0cef-4bf0-be0d-a1cc39bd729e","split_file_version":0,"total_time":5040039,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3492359,"current_duration":3492359},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":5040039,"current_duration":1547680}},"leaf_segments":null,"completed":false},{"id":"26f1e4e8-c594-465c-83cc-4d233ec91565","split_file_version":0,"total_time":5276154,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":4094294,"current_duration":4094294},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":5276154,"current_duration":1181860}},"leaf_segments":null,"completed":false},{"id":"c9d67d12-75dc-4d88-a2c1-cc5d3c202c6b","split_file_version":0,"total_time":5864942,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3783242,"current_duration":3783242},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":5864942,"current_duration":2081700}},"leaf_segments":null,"completed":false},{"id":"8e4762a0-69eb-424b-8db4-9b9258a9e90a","split_file_version":0,"total_time":0,"splits":{},"leaf_segments":null,"completed":false},{"id":"d5e6f04c-491c-4d13-85bd-fd5ac0d29834","split_file_version":0,"total_time":5183477,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3527552,"current_duration":3527552},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":5183477,"current_duration":1655925}},"leaf_segments":null,"completed":false},{"id":"ec6d4173-b4d0-4343-9d0d-fdc9eac54b17","split_file_version":0,"total_time":0,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":4959039,"current_duration":4959039}},"leaf_segments":null,"completed":false},{"id":"850a9b09-7fed-44e5-981b-46aa66f288e5","split_file_version":0,"total_time":4772269,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3671129,"current_duration":3671129},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":4772269,"current_duration":1101140}},"leaf_segments":null,"completed":false},{"id":"05f98b42-75ff-4100-8b38-d9f3694a4944","split_file_version":0,"total_time":5213407,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3908687,"current_duration":3908687},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":5213407,"current_duration":1304720}},"leaf_segments":null,"completed":false},{"id":"36faea64-7549-42db-b67a-467494d7a222","split_file_version":0,"total_time":5895423,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":4018765,"current_duration":4018765},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":5895423,"current_duration":1876657}},"leaf_segments":null,"completed":false},{"id":"d67f52e8-8fb6-46df-b7dc-2f711d0c8755","split_file_version":0,"total_time":5651004,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3105004,"current_duration":3105004},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":5651004,"current_duration":2546000}},"leaf_segments":null,"completed":false},{"id":"8596e95d-5377-4733-be8e-f8c27cdedf7e","split_file_version":0,"total_time":5173226,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3277192,"current_duration":3277192},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":5173226,"current_duration":1896034}},"leaf_segments":null,"completed":false},{"id":"e14e4ed4-3a23-44c5-a9dd-b9d50adde55e","split_file_version":0,"total_time":6003774,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":4053154,"current_duration":4053154},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":6003774,"current_duration":1950620}},"leaf_segments":null,"completed":false},{"id":"a75d5af6-8515-439d-8113-4fab4a23ae20","split_file_version":0,"total_time":4476763,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3006623,"current_duration":3006623},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":4476763,"current_duration":1470140}},"leaf_segments":null,"completed":false},{"id":"d558aa91-ca39-46c9-8253-9fbfdfd7444b","split_file_version":0,"total_time":0,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3698829,"current_duration":3698829}},"leaf_segments":null,"completed":false},{"id":"6efffb6c-7a06-4975-8f5b-05a020a0b02b","split_file_version":0,"total_time":5393626,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3805064,"current_duration":3805064},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":5393626,"current_duration":1588561}},"leaf_segments":null,"completed":false}],"segments":[{"id":"22d0710b-5a73-443d-8fee-9d4725d865c2","name":"GO MODE","gold":3006623,"average":3772468,"pb":3805064,"children":[]},{"id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","name":"KefKILT","gold":1101140,"average":1745918,"pb":1588561,"children":[]}],"sob":4107763,"pb":{"id":"6efffb6c-7a06-4975-8f5b-05a020a0b02b","split_file_version":0,"total_time":5393626,"splits":{"22d0710b-5a73-443d-8fee-9d4725d865c2":{"split_segment_id":"22d0710b-5a73-443d-8fee-9d4725d865c2","current_cumulative":3805064,"current_duration":3805064},"397c03ca-30c5-442a-a40c-4f7f580f6a04":{"split_segment_id":"397c03ca-30c5-442a-a40c-4f7f580f6a04","current_cumulative":5393626,"current_duration":1588561}},"leaf_segments":null,"completed":false},"offset":0,"platform":""} \ No newline at end of file diff --git a/dispatcher/service.go b/dispatcher/service.go index 6165b91..d52673d 100644 --- a/dispatcher/service.go +++ b/dispatcher/service.go @@ -7,7 +7,6 @@ import ( "github.com/zellydev-games/opensplit/command" "github.com/zellydev-games/opensplit/dto" "github.com/zellydev-games/opensplit/logger" - "github.com/zellydev-games/opensplit/repo/adapters" ) const logModule = "dispatcher" @@ -25,6 +24,7 @@ type FolderProvider interface { type RepoProvider interface { LoadSplitFile() (dto.SplitFile, error) SaveSplitFile(dto.SplitFile) error + Export() error } // DispatchReply is sent in response to Dispatch @@ -76,28 +76,5 @@ func (s *Service) OpenSkinsFolder() { } func (s *Service) ExportSplitFile(platform string) error { - sf, err := s.repo.LoadSplitFile() - if err != nil { - return err - } - - _, err = adapters.CleanSplitFile(sf) - if err != nil { - return err - } - - //fileName, err := s.runtime.OpenFileDialog(runtime.OpenDialogOptions{ - // DefaultFilename: fmt.Sprintf("%s-%s-%s.osf", file.GameName, file.GameCategory, platform), - // Title: "Save Exported File", - // Filters: []runtime.FileFilter{{ - // DisplayName: "OpenSplit File", - // Pattern: "*.osf", - // }}, - // CanCreateDirectories: true, - //}) - //if err != nil { - // return err - //} - - return nil + return s.repo.Export() } diff --git a/dto/splitfile.go b/dto/splitfile.go index e54c6ef..7427ef5 100644 --- a/dto/splitfile.go +++ b/dto/splitfile.go @@ -31,4 +31,5 @@ type SplitFile struct { SOB int64 `json:"sob"` PB *Run `json:"pb"` Offset int64 `json:"offset"` + Platform string `json:"platform"` } diff --git a/frontend/src/components/Config.tsx b/frontend/src/components/Config.tsx index 24156e3..70fded1 100644 --- a/frontend/src/components/Config.tsx +++ b/frontend/src/components/Config.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { Dispatch, OpenSkinsFolder, OpenSplitFileFolder } from "../../wailsjs/go/dispatcher/Service"; +import {Dispatch, ExportSplitFile, OpenSkinsFolder, OpenSplitFileFolder} from "../../wailsjs/go/dispatcher/Service"; import { GetAvailableSkins, SetSkin } from "../../wailsjs/go/skin/Service"; import { EventsOn, WindowSetSize } from "../../wailsjs/runtime"; import { Command } from "../App"; diff --git a/frontend/src/components/editor/SplitEditor.tsx b/frontend/src/components/editor/SplitEditor.tsx index 68aae6f..9d928cf 100644 --- a/frontend/src/components/editor/SplitEditor.tsx +++ b/frontend/src/components/editor/SplitEditor.tsx @@ -10,7 +10,7 @@ import { import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React, { useEffect, useRef, useState } from "react"; -import { Dispatch } from "../../../wailsjs/go/dispatcher/Service"; +import {Dispatch, ExportSplitFile} from "../../../wailsjs/go/dispatcher/Service"; import { WindowCenter, WindowSetSize } from "../../../wailsjs/runtime"; import { Command } from "../../App"; import { useClickOutside } from "../../hooks/useClickOutside"; @@ -150,6 +150,9 @@ export default function SplitEditor({ splitFilePayload, speedRunAPIBase }: Split setGameResults([]); }); + // Is this a new file or are we editing? + const editing = splitFilePayload != null; + // Segment stats const [splitFileLoaded] = useState(false); const [gameName, setGameName] = React.useState(splitFilePayload?.game_name ?? ""); @@ -162,6 +165,9 @@ export default function SplitEditor({ splitFilePayload, speedRunAPIBase }: Split const [gameResults, setGameResults] = React.useState([]); const timeoutID = useRef(0); + // Exporter + const [platform, setPlatform] = React.useState(splitFilePayload?.platform ?? "SNES"); + // Position and size the edit window useEffect(() => { WindowSetSize(1000, 900); @@ -272,6 +278,7 @@ export default function SplitEditor({ splitFilePayload, speedRunAPIBase }: Split pb: splitFilePayload?.pb ?? null, sob: splitFilePayload?.sob ?? 0, offset: offsetMS, + platform: platform, }); const payload = JSON.stringify(newSplitFilePayload); @@ -516,7 +523,7 @@ export default function SplitEditor({ splitFilePayload, speedRunAPIBase }: Split return (
-

{splitFileLoaded ? "Editing Split File" : "New Split File"}

+

{editing ? "Editing Split File" : "New Split File"}

@@ -573,6 +580,64 @@ export default function SplitEditor({ splitFilePayload, speedRunAPIBase }: Split />
+
+ + +
+
+
+ +
+