Adding modding folder with a hacked build.
This commit is contained in:
@@ -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) {
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user