Files
galaxy-game/client/loader/loader.go
T
Ilia Denisov 5029857fe4 world refactor
2026-03-17 12:48:05 +03:00

215 lines
4.5 KiB
Go

package loader
import (
"context"
"errors"
"fmt"
"sync"
"galaxy/client/updater"
"galaxy/connector"
"galaxy/storage"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
const (
loaderLogViewportColumns = 80
loaderLogViewportRows = 12
)
type loader struct {
app fyne.App
storage storage.Storage
connector connector.Connector
updater *updater.Manager
runner uiRunner
debugWindow fyne.Window
textGrid *widget.TextGrid
btn *widget.Button
ctx context.Context
resultMu sync.Mutex
result error
closeMu sync.Mutex
closeQuits bool
}
// loaderLogViewportMinSize derives a stable monospace TextGrid viewport size
// from the active Fyne text metrics.
func loaderLogViewportMinSize(app fyne.App) fyne.Size {
if app == nil || app.Driver() == nil {
return fyne.NewSize(0, 0)
}
cellSize, _ := app.Driver().RenderedTextSize(
"M",
theme.TextSize(),
fyne.TextStyle{Monospace: true},
nil,
)
return fyne.NewSize(
cellSize.Width*loaderLogViewportColumns,
cellSize.Height*loaderLogViewportRows,
)
}
func NewLoader(s storage.Storage, conn connector.Connector, app fyne.App) (*loader, error) {
l := &loader{
app: app,
connector: conn,
storage: s,
updater: updater.NewManager(s, conn),
runner: execRunner{},
textGrid: widget.NewTextGrid(),
debugWindow: app.NewWindow("Loader"),
}
l.btn = widget.NewButton("Retry", l.onButtonAction)
l.btn.Disable()
l.textGrid.Scroll = fyne.ScrollNone
l.debugWindow.SetCloseIntercept(l.onWindowClose)
logScroll := container.NewScroll(l.textGrid)
logScroll.Direction = container.ScrollBoth
logScroll.SetMinSize(loaderLogViewportMinSize(app))
actionBar := container.NewCenter(container.NewHBox(l.btn))
content := container.NewBorder(nil, actionBar, nil, nil, logScroll)
l.debugWindow.SetContent(content)
l.debugWindow.Resize(content.MinSize())
l.debugWindow.SetFixedSize(true)
l.debugWindow.CenterOnScreen()
return l, nil
}
func (l *loader) runOnce(ctx context.Context) error {
target, err := l.updater.EnsureLaunchTarget()
if err != nil {
return err
}
l.logText(fmt.Sprintf("Starting UI client v%s", target.Version))
l.logText(fmt.Sprintf("Executable: %s", target.Path))
exitCode, runErr := l.runner.Run(ctx, target.Path)
markErr := l.updater.MarkLaunchResult(target.Version, exitCode, runErr)
switch {
case runErr != nil:
return errors.Join(fmt.Errorf("launch UI client v%s: %w", target.Version, runErr), markErr)
case exitCode != 0:
return errors.Join(fmt.Errorf("UI client v%s exited with code %d", target.Version, exitCode), markErr)
default:
return markErr
}
}
// init prepares and launches the standalone UI client, or shows a retry button on failure.
func (l *loader) init(ctx context.Context) {
l.setCloseQuits(false)
fyne.Do(func() {
l.textGrid.SetText("")
l.btn.Hide()
l.btn.Disable()
// show debugWindow can be done with future debug mode, e.g. with -debug flag
l.debugWindow.Hide()
})
err := l.runOnce(ctx)
if err == nil || errors.Is(err, context.Canceled) {
l.setResult(nil)
fyne.Do(func() {
l.debugWindow.Hide()
l.app.Quit()
})
return
}
l.setCloseQuits(true)
l.setResult(err)
l.logError(err)
fyne.Do(func() {
l.btn.SetText("Retry")
l.btn.Enable()
l.btn.Show()
l.debugWindow.Show()
})
}
func (l *loader) onButtonAction() {
if l.ctx == nil {
return
}
go l.init(l.ctx)
}
func (l *loader) onWindowClose() {
if l.getCloseQuits() {
l.app.Quit()
return
}
l.debugWindow.Hide()
}
func (l *loader) logText(v string) {
if l.textGrid == nil {
return
}
fyne.Do(func() { l.textGrid.Append(v) })
}
func (l *loader) logError(err error) {
l.logText(fmt.Sprintf("ERROR: %s", err))
}
func (l *loader) setResult(err error) {
l.resultMu.Lock()
defer l.resultMu.Unlock()
l.result = err
}
func (l *loader) getResult() error {
l.resultMu.Lock()
defer l.resultMu.Unlock()
return l.result
}
func (l *loader) setCloseQuits(v bool) {
l.closeMu.Lock()
defer l.closeMu.Unlock()
l.closeQuits = v
}
func (l *loader) getCloseQuits() bool {
l.closeMu.Lock()
defer l.closeMu.Unlock()
return l.closeQuits
}
// Run starts the loader window, launches the standalone UI process, and returns
// the final launch result once the loader application exits.
func (l *loader) Run(ctx context.Context) error {
l.ctx = ctx
go l.init(ctx)
go func() {
<-ctx.Done()
fyne.Do(l.app.Quit)
}()
l.app.Run()
if errors.Is(ctx.Err(), context.Canceled) {
return nil
}
return l.getResult()
}