Adding modding folder with a hacked build.

This commit is contained in:
ml
2026-09-26 08:53:08 +02:00
parent cdb960dc71
commit c91080aba7
755 changed files with 50424 additions and 0 deletions
@@ -0,0 +1,23 @@
package project
import (
"github.com/inkyblackness/imgui-go/v3"
"github.com/inkyblackness/hacked/ui/gui"
)
type addManifestEntryStartState struct {
machine gui.ModalStateMachine
view *View
}
func (state addManifestEntryStartState) Render() {
imgui.OpenPopup("Add static world data")
state.machine.SetState(&addManifestEntryWaitingState{
machine: state.machine,
view: state.view,
})
}
func (state addManifestEntryStartState) HandleFiles(names []string) {
}
@@ -0,0 +1,61 @@
package project
import (
"time"
"github.com/inkyblackness/imgui-go/v3"
"github.com/sqweek/dialog"
"github.com/inkyblackness/hacked/ui/gui"
)
type addManifestEntryWaitingState struct {
machine gui.ModalStateMachine
view *View
failureTime time.Time
}
func (state *addManifestEntryWaitingState) Render() {
if imgui.BeginPopupModalV("Add static world data", nil,
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
imgui.Text("Waiting for folders/files.")
if !state.failureTime.IsZero() {
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
imgui.Text("Previous attempt failed, no usable data detected.\nPlease check and try again.")
imgui.PopStyleColor()
if time.Since(state.failureTime).Seconds() > 5 {
state.failureTime = time.Time{}
}
}
imgui.Text(`From your file browser drag'n'drop the folder (or files)
of the static data you want to reference into the editor window.
Typically, you would use the main "data" directory of the game
(where all the .res files are).
`)
imgui.Separator()
if imgui.Button("Browse...") {
dlgBuilder := dialog.Directory()
filename, err := dlgBuilder.Browse()
if err == nil {
state.HandleFiles([]string{filename})
}
}
imgui.SameLine()
if imgui.Button("Cancel") {
state.machine.SetState(nil)
imgui.CloseCurrentPopup()
}
imgui.EndPopup()
} else {
state.machine.SetState(nil)
}
}
func (state *addManifestEntryWaitingState) HandleFiles(names []string) {
err := state.view.tryAddManifestEntryFrom(names)
if err != nil {
state.failureTime = time.Now()
} else {
state.machine.SetState(nil)
}
}
@@ -0,0 +1,23 @@
package project
import (
"github.com/inkyblackness/imgui-go/v3"
"github.com/inkyblackness/hacked/ui/gui"
)
type loadModStartState struct {
machine gui.ModalStateMachine
view *View
}
func (state loadModStartState) Render() {
imgui.OpenPopup("Load mod")
state.machine.SetState(&loadModWaitingState{
machine: state.machine,
view: state.view,
})
}
func (state loadModStartState) HandleFiles(names []string) {
}
@@ -0,0 +1,62 @@
package project
import (
"time"
"github.com/inkyblackness/imgui-go/v3"
"github.com/sqweek/dialog"
"github.com/inkyblackness/hacked/ui/gui"
)
type loadModWaitingState struct {
machine gui.ModalStateMachine
view *View
failureTime time.Time
}
func (state *loadModWaitingState) Render() {
if imgui.BeginPopupModalV("Load mod", nil,
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
imgui.Text("Waiting for folder.")
if !state.failureTime.IsZero() {
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
imgui.Text("Previous attempt failed, no usable data detected.\nPlease check and try again.")
imgui.PopStyleColor()
if time.Since(state.failureTime).Seconds() > 5 {
state.failureTime = time.Time{}
}
}
imgui.Text(`From your file browser drag'n'drop the folder
of the mod you want to work on into the editor window.
If you want to modify the main game files,
use the main "data" directory of the game.
`)
imgui.Text("This action will clear the undo/redo buffer\nand you will lose any unsaved changes.")
imgui.Separator()
if imgui.Button("Browse...") {
dlgBuilder := dialog.Directory()
filename, err := dlgBuilder.Browse()
if err == nil {
state.HandleFiles([]string{filename})
}
}
imgui.SameLine()
if imgui.Button("Cancel") {
state.machine.SetState(nil)
imgui.CloseCurrentPopup()
}
imgui.EndPopup()
} else {
state.machine.SetState(nil)
}
}
func (state *loadModWaitingState) HandleFiles(names []string) {
err := state.view.tryLoadModFrom(names)
if err != nil {
state.failureTime = time.Now()
} else {
state.machine.SetState(nil)
}
}
@@ -0,0 +1,23 @@
package project
import (
"github.com/inkyblackness/imgui-go/v3"
"github.com/inkyblackness/hacked/ui/gui"
)
type saveModAsStartState struct {
machine gui.ModalStateMachine
view *View
}
func (state saveModAsStartState) Render() {
imgui.OpenPopup("Save mod as")
state.machine.SetState(&saveModAsWaitingState{
machine: state.machine,
view: state.view,
})
}
func (state saveModAsStartState) HandleFiles(names []string) {
}
@@ -0,0 +1,82 @@
package project
import (
"os"
"time"
"github.com/inkyblackness/imgui-go/v3"
"github.com/sqweek/dialog"
"github.com/inkyblackness/hacked/ui/gui"
)
type saveModAsWaitingState struct {
machine gui.ModalStateMachine
view *View
failureTime time.Time
errorInfo string
}
func (state *saveModAsWaitingState) Render() {
if imgui.BeginPopupModalV("Save mod as", nil,
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
imgui.Text("Waiting for folder.")
if !state.failureTime.IsZero() {
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
imgui.Text("Previous attempt failed, not a proper folder.\nPlease check and try again.")
if len(state.errorInfo) > 0 {
imgui.Text(state.errorInfo)
}
imgui.PopStyleColor()
if time.Since(state.failureTime).Seconds() > 5 {
state.failureTime = time.Time{}
}
}
imgui.Text(`From your file browser drag'n'drop the folder
where to save the data into the editor window.
`)
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
imgui.Text("It is recommended to use an empty folder.\nSaving will potentially overwrite existing files.")
imgui.PopStyleColor()
imgui.Separator()
if imgui.Button("Browse...") {
dlgBuilder := dialog.Directory()
filename, err := dlgBuilder.Browse()
if err == nil {
state.HandleFiles([]string{filename})
}
}
imgui.SameLine()
if imgui.Button("Cancel") {
state.machine.SetState(nil)
imgui.CloseCurrentPopup()
}
imgui.EndPopup()
} else {
state.machine.SetState(nil)
}
}
func (state *saveModAsWaitingState) HandleFiles(names []string) {
modPath, ok := state.verifyDir(names)
if ok {
state.machine.SetState(nil)
state.view.requestSaveMod(modPath)
} else {
state.failureTime = time.Now()
}
}
func (state saveModAsWaitingState) verifyDir(names []string) (string, bool) {
if len(names) != 1 {
return "", false
}
fileInfo, err := os.Stat(names[0])
if err != nil {
return "", false
}
if !fileInfo.IsDir() {
return "", false
}
return names[0], true
}
@@ -0,0 +1,28 @@
package project
import (
"time"
"github.com/inkyblackness/imgui-go/v3"
"github.com/inkyblackness/hacked/ui/gui"
)
type saveModFailedState struct {
machine gui.ModalStateMachine
view *View
errorInfo string
}
func (state saveModFailedState) Render() {
imgui.OpenPopup("Save mod as")
state.machine.SetState(&saveModAsWaitingState{
machine: state.machine,
view: state.view,
failureTime: time.Now(),
errorInfo: state.errorInfo,
})
}
func (state saveModFailedState) HandleFiles(names []string) {
}
+246
View File
@@ -0,0 +1,246 @@
package project
import (
"fmt"
"github.com/inkyblackness/imgui-go/v3"
"github.com/inkyblackness/hacked/ss1/edit"
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
"github.com/inkyblackness/hacked/ss1/world"
"github.com/inkyblackness/hacked/ui/gui"
)
// View handles the project display.
type View struct {
service *edit.ProjectService
modalStateMachine gui.ModalStateMachine
guiScale float32
commander cmd.Registry
model viewModel
}
// NewView creates a new instance for the project display.
func NewView(service *edit.ProjectService, modalStateMachine gui.ModalStateMachine, guiScale float32, commander cmd.Registry) *View {
return &View{
service: service,
modalStateMachine: modalStateMachine,
guiScale: guiScale,
commander: commander,
model: freshViewModel(),
}
}
// WindowOpen returns the flag address, to be used with the main menu.
func (view *View) WindowOpen() *bool {
return &view.model.windowOpen
}
// Render requests to render the view.
func (view *View) Render() {
if view.model.restoreFocus {
imgui.SetNextWindowFocus()
view.model.restoreFocus = false
view.model.windowOpen = true
}
title := "Project"
saveStatus := view.service.SaveStatus()
if saveStatus.FilesModified > 0 {
title += fmt.Sprintf(" - %d file(s) pending save", saveStatus.FilesModified)
if saveStatus.SavePending && (saveStatus.SaveIn.Seconds() < 4) {
title += fmt.Sprintf(" - auto-save in %d", int(saveStatus.SaveIn.Seconds()))
}
}
if view.model.windowOpen {
imgui.SetNextWindowSizeV(imgui.Vec2{X: 400 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
if imgui.BeginV(title+"###Project", view.WindowOpen(), 0) {
view.renderContent()
}
imgui.End()
}
}
func (view *View) renderContent() {
imgui.Text("Mod Location")
imgui.PushStyleVarVec2(imgui.StyleVarWindowPadding, imgui.Vec2{X: 1, Y: 0})
imgui.BeginChildV("ModLocation", imgui.Vec2{X: -200*view.guiScale - 10*view.guiScale, Y: imgui.TextLineHeight() * 1.5}, true,
imgui.WindowFlagsNoScrollbar|imgui.WindowFlagsNoScrollWithMouse)
modPath := view.service.ModPath()
if len(modPath) > 0 {
imgui.Text(modPath)
} else {
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1.0, Y: 1.0, Z: 1.0, W: 0.5})
imgui.Text("(new mod)")
imgui.PopStyleColor()
}
imgui.EndChild()
imgui.PopStyleVar()
imgui.BeginGroup()
imgui.SameLine()
if imgui.ButtonV("Save", imgui.Vec2{X: 100 * view.guiScale, Y: 0}) {
view.StartSavingMod()
}
imgui.SameLine()
if imgui.ButtonV("Load...", imgui.Vec2{X: 100 * view.guiScale, Y: 0}) {
view.startLoadingMod()
}
imgui.EndGroup()
imgui.Text("Static World Data")
imgui.BeginChildV("ManifestEntries", imgui.Vec2{X: -100 * view.guiScale, Y: 0}, true, 0)
manifest := view.service.Mod().World()
entries := manifest.EntryCount()
for i := entries - 1; i >= 0; i-- {
entry, _ := manifest.Entry(i)
if imgui.SelectableV(entry.ID, view.model.selectedManifestEntry == i, 0, imgui.Vec2{}) {
view.model.selectedManifestEntry = i
}
}
imgui.EndChild()
imgui.SameLine()
imgui.BeginGroup()
if imgui.ButtonV("Add...", imgui.Vec2{X: -1, Y: 0}) {
view.startAddingManifestEntry()
}
if imgui.ButtonV("Up", imgui.Vec2{X: -1, Y: 0}) {
view.requestMoveManifestEntryUp()
}
if imgui.ButtonV("Down", imgui.Vec2{X: -1, Y: 0}) {
view.requestMoveManifestEntryDown()
}
if imgui.ButtonV("Remove", imgui.Vec2{X: -1, Y: 0}) {
view.requestRemoveManifestEntry()
}
imgui.EndGroup()
}
func (view *View) startLoadingMod() {
view.modalStateMachine.SetState(&loadModStartState{
machine: view.modalStateMachine,
view: view,
})
}
// StartSavingMod initiates to save the mod.
// It either opens the save-as dialog, or simply saves under the current folder.
func (view *View) StartSavingMod() {
if view.service.ModHasStorageLocation() {
err := view.service.SaveMod()
if err != nil {
view.handleSaveModFailure(err)
}
} else {
view.modalStateMachine.SetState(&saveModAsStartState{
machine: view.modalStateMachine,
view: view,
})
}
}
func (view *View) startAddingManifestEntry() {
view.modalStateMachine.SetState(&addManifestEntryStartState{
machine: view.modalStateMachine,
view: view,
})
}
func (view *View) requestMoveManifestEntryUp() {
manifest := view.service.Mod().World()
entries := manifest.EntryCount()
if (view.model.selectedManifestEntry >= 0) && (view.model.selectedManifestEntry < (entries - 1)) {
view.requestMoveManifestEntry(view.model.selectedManifestEntry+1, view.model.selectedManifestEntry)
}
}
func (view *View) requestMoveManifestEntryDown() {
if view.model.selectedManifestEntry > 0 {
view.requestMoveManifestEntry(view.model.selectedManifestEntry-1, view.model.selectedManifestEntry)
}
}
func (view *View) requestMoveManifestEntry(to, from int) {
_ = view.commander.Register(
cmd.Named("moveManifestEntry"),
cmd.Reverse(view.taskToRestoreFocus()),
cmd.Reverse(view.taskToSelectEntry(from)),
cmd.Nested(func() error { return view.service.MoveManifestEntry(to, from) }),
cmd.Forward(view.taskToSelectEntry(to)),
cmd.Forward(view.taskToRestoreFocus()),
)
}
func (view *View) tryAddManifestEntryFrom(names []string) error {
entry, err := world.NewManifestEntryFrom(names)
if err != nil {
return err
}
view.requestAddManifestEntry(entry)
return nil
}
func (view *View) requestAddManifestEntry(entry *world.ManifestEntry) {
at := view.model.selectedManifestEntry + 1
_ = view.commander.Register(
cmd.Named("addManifestEntry"),
cmd.Reverse(view.taskToRestoreFocus()),
cmd.Reverse(view.taskToSelectEntry(view.model.selectedManifestEntry)),
cmd.Nested(func() error { return view.service.AddManifestEntry(at, entry) }),
cmd.Forward(view.taskToSelectEntry(at)),
cmd.Forward(view.taskToRestoreFocus()),
)
}
func (view *View) requestRemoveManifestEntry() {
manifest := view.service.Mod().World()
at := view.model.selectedManifestEntry
if (at < 0) || (at >= manifest.EntryCount()) {
return
}
_ = view.commander.Register(
cmd.Named("removeManifestEntry"),
cmd.Reverse(view.taskToRestoreFocus()),
cmd.Reverse(view.taskToSelectEntry(view.model.selectedManifestEntry)),
cmd.Nested(func() error { return view.service.RemoveManifestEntry(at) }),
cmd.Forward(view.taskToSelectEntry(view.model.selectedManifestEntry-1)),
cmd.Forward(view.taskToRestoreFocus()),
)
}
func (view *View) tryLoadModFrom(names []string) error {
return view.service.TryLoadModFrom(names)
}
func (view *View) requestSaveMod(modPath string) {
err := view.service.SaveModUnder(modPath)
if err != nil {
view.handleSaveModFailure(err)
}
}
func (view *View) handleSaveModFailure(err error) {
view.modalStateMachine.SetState(&saveModFailedState{
machine: view.modalStateMachine,
view: view,
errorInfo: err.Error(),
})
}
func (view *View) taskToRestoreFocus() cmd.Task {
return func(modder world.Modder) error {
view.model.restoreFocus = true
return nil
}
}
func (view *View) taskToSelectEntry(index int) cmd.Task {
return func(modder world.Modder) error {
view.model.selectedManifestEntry = index
return nil
}
}
@@ -0,0 +1,14 @@
package project
type viewModel struct {
restoreFocus bool
windowOpen bool
selectedManifestEntry int
}
func freshViewModel() viewModel {
return viewModel{
windowOpen: false,
selectedManifestEntry: -1,
}
}