Files
questshock/modding/hacked/editor/Application.go
T

946 lines
31 KiB
Go

package editor
import (
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"github.com/inkyblackness/imgui-go/v3"
"github.com/inkyblackness/hacked/editor/about"
"github.com/inkyblackness/hacked/editor/animations"
"github.com/inkyblackness/hacked/editor/archives"
"github.com/inkyblackness/hacked/editor/bitmaps"
"github.com/inkyblackness/hacked/editor/external"
"github.com/inkyblackness/hacked/editor/graphics"
"github.com/inkyblackness/hacked/editor/levels"
"github.com/inkyblackness/hacked/editor/messages"
"github.com/inkyblackness/hacked/editor/movies"
"github.com/inkyblackness/hacked/editor/objects"
"github.com/inkyblackness/hacked/editor/project"
"github.com/inkyblackness/hacked/editor/render"
"github.com/inkyblackness/hacked/editor/sounds"
"github.com/inkyblackness/hacked/editor/texts"
"github.com/inkyblackness/hacked/editor/textures"
"github.com/inkyblackness/hacked/ss1"
"github.com/inkyblackness/hacked/ss1/content/archive/level"
"github.com/inkyblackness/hacked/ss1/content/bitmap"
"github.com/inkyblackness/hacked/ss1/content/movie"
"github.com/inkyblackness/hacked/ss1/content/sound"
"github.com/inkyblackness/hacked/ss1/content/text"
"github.com/inkyblackness/hacked/ss1/edit"
"github.com/inkyblackness/hacked/ss1/edit/media"
"github.com/inkyblackness/hacked/ss1/edit/undoable"
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
"github.com/inkyblackness/hacked/ss1/resource"
"github.com/inkyblackness/hacked/ss1/world"
"github.com/inkyblackness/hacked/ss1/world/ids"
"github.com/inkyblackness/hacked/ui/gui"
"github.com/inkyblackness/hacked/ui/input"
"github.com/inkyblackness/hacked/ui/opengl"
)
const (
errCouldNotOpenFile ss1.StringError = "could not open file"
errCouldNotReadFile ss1.StringError = "could not read file"
)
type projectState struct {
ProjectSettings *edit.ProjectSettings `json:",omitempty"`
GameStateSettings *edit.GameStateSettings `json:",omitempty"`
OpenWindows []string `json:",omitempty"`
ActiveLevelIndex *int `json:",omitempty"`
}
func projectStateFromFile(filename string) (projectState, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return projectState{}, errCouldNotOpenFile
}
var state projectState
err = json.Unmarshal(data, &state)
if err != nil {
return projectState{}, errCouldNotReadFile
}
return state, nil
}
// SaveTo stores the state in a file with given filename.
func (state projectState) SaveTo(filename string) error {
bytes, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(filename, bytes, 0640)
}
type lastProjectState struct {
projectState
LastProject string `json:",omitempty"`
}
func lastProjectStateFromFile(filename string) lastProjectState {
data, err := ioutil.ReadFile(filename)
if err != nil {
return lastProjectState{}
}
var state lastProjectState
err = json.Unmarshal(data, &state)
if err != nil {
return lastProjectState{}
}
return state
}
// SaveTo stores the state in a file with given filename.
func (state lastProjectState) SaveTo(filename string) error {
bytes, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(filename, bytes, 0640)
}
// Application is the root object of the graphical editor.
// It is set up by the main method.
type Application struct {
// Version identifies the build of the application.
Version string
// ConfigDir is the base path to store configuration in.
ConfigDir string
window opengl.Window
clipboard clipboardAdapter
gl opengl.OpenGL
// FontFile specifies the font to use.
FontFile string
// FontSize specifies the font size to use.
FontSize float32
// GuiScale is applied when the window is initialized.
GuiScale float32
guiContext *gui.Context
lastModifier input.Modifier
lastMouseX float32
lastMouseY float32
txnBuilder cmd.TransactionBuilder
cmdStack *cmd.Stack
mod *world.Mod
cp text.Codepage
textLineCache *text.Cache
textPageCache *text.Cache
messagesCache *text.ElectronicMessageCache
paletteCache *graphics.PaletteCache
textureCache *graphics.TextureCache
frameCache *graphics.FrameCache
animationCache *bitmap.AnimationCache
movieCache *movie.Cache
soundEffectCache *sound.EffectCache
mapDisplay *levels.MapDisplay
levels *edit.EditableLevels
levelSelection *edit.LevelSelectionService
levelEditorService *edit.LevelEditorService
projectService *edit.ProjectService
gameStateService *edit.GameStateService
gameObjectsService *edit.GameObjectsService
projectView *project.View
archiveView *archives.View
levelControlView *levels.ControlView
levelTilesView *levels.TilesView
levelObjectsView *levels.ObjectsView
messagesView *messages.View
textsView *texts.View
bitmapsView *bitmaps.View
texturesView *textures.View
animationsView *animations.View
moviesView *movies.View
soundEffectsView *sounds.View
objectsView *objects.View
aboutView *about.View
licensesView *about.LicensesView
modalState gui.ModalStateWrapper
failureMessage string
failurePending bool
}
// InitializeWindow takes the given window and attaches the callbacks.
func (app *Application) InitializeWindow(window opengl.Window) (err error) {
app.window = window
app.clipboard.window = window
app.gl = window.OpenGL()
app.window.RestoreState(opengl.WindowStateFromFile(app.windowStateConfigFilename(), app.window.StateSnapshot()))
app.initSignalling()
app.initWindowCallbacks()
app.initOpenGL()
app.initModel()
err = app.initGui()
if err != nil {
return
}
app.initView()
app.restoreWorkspace()
app.onWindowResize(app.window.Size())
return
}
func (app *Application) onWindowClosed() {
if app.guiContext != nil {
app.guiContext.Destroy()
app.guiContext = nil
}
}
func (app *Application) initWindowCallbacks() {
app.window.OnClosing(app.onWindowClosing)
app.window.OnClosed(app.onWindowClosed)
app.window.OnFileDropCallback(app.onFilesDropped)
app.window.OnResize(app.onWindowResize)
app.window.OnKeyPress(app.onKeyPress)
app.window.OnKeyRelease(app.onKeyRelease)
app.window.OnCharCallback(app.onChar)
app.window.OnModifier(app.onModifier)
app.window.OnMouseMove(app.onMouseMove)
app.window.OnMouseScroll(app.onMouseScroll)
app.window.OnMouseButtonDown(app.onMouseButtonDown)
app.window.OnMouseButtonUp(app.onMouseButtonUp)
app.window.OnRender(app.render)
}
func (app *Application) render() {
app.guiContext.NewFrame()
width, height := app.window.Size()
app.gl.Viewport(0, 0, int32(width), int32(height))
app.gl.ClearColor(0.0, 0.0, 0.0, 1.0)
app.gl.Clear(opengl.COLOR_BUFFER_BIT)
paletteTexture, _ := app.paletteCache.Palette(0)
app.mapDisplay.Render(
paletteTexture, app.textureCache.Texture,
app.levelTilesView.TextureDisplay(), app.levelTilesView.ColorDisplay())
app.handleFailure()
app.renderMainMenu()
app.projectView.Render()
app.updateAutoSave()
app.archiveView.Render()
app.levelControlView.Render()
app.levelTilesView.Render()
app.levelObjectsView.Render()
app.messagesView.Render()
app.textsView.Render()
app.bitmapsView.Render()
app.texturesView.Render()
app.animationsView.Render()
app.moviesView.Render()
app.soundEffectsView.Render()
app.objectsView.Render()
app.aboutView.Render()
app.licensesView.Render()
app.modalState.Render()
app.guiContext.Render(app.bitmapTextureForUI)
}
func (app *Application) initOpenGL() {
app.gl.Disable(opengl.DEPTH_TEST)
app.gl.Enable(opengl.BLEND)
app.gl.BlendFunc(opengl.SRC_ALPHA, opengl.ONE_MINUS_SRC_ALPHA)
}
func (app *Application) initGui() (err error) {
app.initGuiSizes()
param := gui.ContextParameters{
ConfigDir: app.ConfigDir,
FontFile: app.FontFile,
FontSize: app.FontSize,
}
app.guiContext, err = gui.NewContext(app.window, param)
if err != nil {
return
}
imguiIO := imgui.CurrentIO()
imguiIO.KeyMap(imgui.KeyTab, int(input.KeyTab))
imguiIO.KeyMap(imgui.KeyLeftArrow, int(input.KeyLeft))
imguiIO.KeyMap(imgui.KeyRightArrow, int(input.KeyRight))
imguiIO.KeyMap(imgui.KeyUpArrow, int(input.KeyUp))
imguiIO.KeyMap(imgui.KeyDownArrow, int(input.KeyDown))
imguiIO.KeyMap(imgui.KeyPageUp, int(input.KeyPageUp))
imguiIO.KeyMap(imgui.KeyPageDown, int(input.KeyPageDown))
imguiIO.KeyMap(imgui.KeyHome, int(input.KeyHome))
imguiIO.KeyMap(imgui.KeyEnd, int(input.KeyEnd))
imguiIO.KeyMap(imgui.KeyInsert, int(input.KeyInsert))
imguiIO.KeyMap(imgui.KeyDelete, int(input.KeyDelete))
imguiIO.KeyMap(imgui.KeyBackspace, int(input.KeyBackspace))
imguiIO.KeyMap(imgui.KeyEnter, int(input.KeyEnter))
imguiIO.KeyMap(imgui.KeyEscape, int(input.KeyEscape))
imguiIO.KeyMap(imgui.KeyA, int(input.KeyA))
imguiIO.KeyMap(imgui.KeyC, int(input.KeyC))
imguiIO.KeyMap(imgui.KeyV, int(input.KeyV))
imguiIO.KeyMap(imgui.KeyX, int(input.KeyX))
app.initGuiStyle()
app.mapDisplay = levels.NewMapDisplay(app.gameObjectsService, app.levelSelection, app.levelEditorService,
app.gl, app.GuiScale,
app.gameTexture)
return
}
func (app *Application) gameTexture(index level.TextureIndex) (*graphics.BitmapTexture, error) {
key := resource.KeyOf(ids.LargeTextures.Plus(int(index)), resource.LangAny, 0)
return app.textureCache.Texture(key)
}
func (app *Application) bitmapTextureForUI(textureID imgui.TextureID) (palette uint32, texture uint32) {
idType := byte(textureID >> 48)
switch idType {
case render.BitmapTextureTypeResource:
paletteTexture, _ := app.paletteCache.Palette(0)
if paletteTexture == nil {
return 0, 0
}
lang := resource.Language((textureID >> 32) & 0xFF)
resourceID := resource.ID((textureID >> 16) & 0xFFFF)
blockIndex := int(textureID & 0xFFFF)
key := resource.KeyOf(resourceID, lang, blockIndex)
tex, err := app.textureCache.Texture(key)
if err != nil {
return 0, 0
}
return paletteTexture.Handle(), tex.Handle()
case render.BitmapTextureTypeFrame:
key := graphics.FrameCacheKey(textureID & 0xFFFF)
return app.frameCache.HandlesForKey(key)
}
return 0, 0
}
func (app *Application) onWindowClosing() {
windowState := app.window.StateSnapshot()
_ = windowState.SaveTo(app.windowStateConfigFilename())
app.saveWorkspace()
}
func (app *Application) onWindowResize(width int, height int) {
app.mapDisplay.WindowResized(width, height)
app.gl.Viewport(0, 0, int32(width), int32(height))
}
func (app *Application) onFilesDropped(names []string) {
app.modalState.HandleFiles(names)
}
func (app *Application) onKeyPress(key input.Key, modifier input.Modifier) {
app.lastModifier = modifier
usedGlobalShortcut := true
switch {
case key == input.KeyEscape:
app.modalState.SetState(nil)
case key == input.KeyUndo:
app.tryUndo()
case key == input.KeyRedo:
app.tryRedo()
case key == input.KeyNew:
app.newProject()
case key == input.KeySave:
app.projectView.StartSavingMod()
case key == input.KeyF1 && modifier.IsClear():
*app.projectView.WindowOpen() = !*app.projectView.WindowOpen()
case key == input.KeyF2 && modifier.IsClear():
*app.levelControlView.WindowOpen() = !*app.levelControlView.WindowOpen()
case key == input.KeyF3 && modifier.IsClear():
*app.levelTilesView.WindowOpen() = !*app.levelTilesView.WindowOpen()
case key == input.KeyF4 && modifier.IsClear():
*app.levelObjectsView.WindowOpen() = !*app.levelObjectsView.WindowOpen()
case key == input.KeyF5 && modifier.IsClear():
*app.messagesView.WindowOpen() = !*app.messagesView.WindowOpen()
default:
usedGlobalShortcut = false
}
if usedGlobalShortcut {
return
}
if app.shouldReportKeyToImgui(key) {
imgui.CurrentIO().KeyPress(int(key))
app.updateKeyModifier()
}
}
func (app *Application) onKeyRelease(key input.Key, modifier input.Modifier) {
app.lastModifier = modifier
if app.shouldReportKeyToImgui(key) {
imgui.CurrentIO().KeyRelease(int(key))
app.updateKeyModifier()
}
}
func (app *Application) shouldReportKeyToImgui(key input.Key) bool {
keyIsModifier := key.AsModifier() != input.ModNone
return keyIsModifier || app.guiContext.IsUsingKeyboard()
}
func (app *Application) onChar(char rune) {
if app.guiContext.IsUsingKeyboard() {
imgui.CurrentIO().AddInputCharacters(string(char))
return
}
switch char {
case 'v':
_ = app.levelEditorService.PlaceObjectsOnFloor()
case 'f':
_ = app.levelEditorService.PlaceObjectsOnEyeLevel()
case 'r':
_ = app.levelEditorService.PlaceObjectsOnCeiling()
}
}
func (app *Application) updateKeyModifier() {
imguiIO := imgui.CurrentIO()
imguiIO.KeyCtrl(int(input.KeyControl), int(input.KeyControl))
imguiIO.KeyShift(int(input.KeyShift), int(input.KeyShift))
imguiIO.KeyAlt(int(input.KeyAlt), int(input.KeyAlt))
imguiIO.KeySuper(int(input.KeySuper), int(input.KeySuper))
}
func (app *Application) onModifier(modifier input.Modifier) {
app.lastModifier = modifier
}
func (app *Application) modalActive() bool {
return (app.modalState.State != nil) || (len(app.failureMessage) > 0)
}
func (app *Application) tryUndo() {
if !app.cmdStack.CanUndo() || app.modalActive() {
return
}
imgui.ClearActiveID()
err := app.projectService.ModifyModWith(app.cmdStack.Undo)
if err != nil {
app.onFailure("Undo", "", err)
}
}
func (app *Application) tryRedo() {
if !app.cmdStack.CanRedo() || app.modalActive() {
return
}
imgui.ClearActiveID()
err := app.projectService.ModifyModWith(app.cmdStack.Redo)
if err != nil {
app.onFailure("Redo", "", err)
}
}
func (app *Application) onMouseMove(x, y float32) {
app.lastMouseX = x
app.lastMouseY = y
app.guiContext.SetMousePosition(x, y)
if !app.guiContext.IsUsingMouse() {
app.mapDisplay.MouseMoved(x, y)
}
}
func (app *Application) onMouseScroll(dx, dy float32) {
if !app.guiContext.IsUsingMouse() {
app.mapDisplay.MouseScrolled(app.lastMouseX, app.lastMouseY, dx, dy, app.lastModifier)
}
app.guiContext.MouseScroll(dx, dy)
}
func (app *Application) onMouseButtonDown(buttonMask uint32, modifier input.Modifier) {
if !app.guiContext.IsUsingMouse() {
app.mapDisplay.MouseButtonDown(app.lastMouseX, app.lastMouseY, buttonMask)
}
app.reportButtonChange(buttonMask, true)
}
func (app *Application) onMouseButtonUp(buttonMask uint32, modifier input.Modifier) {
if !app.guiContext.IsUsingMouse() {
app.mapDisplay.MouseButtonUp(app.lastMouseX, app.lastMouseY, buttonMask, modifier)
}
app.reportButtonChange(buttonMask, false)
}
func (app *Application) reportButtonChange(buttonMask uint32, down bool) {
for buttonIndex := 0; buttonIndex < 32; buttonIndex++ {
if (buttonMask & (1 << uint32(buttonIndex))) != 0 {
app.guiContext.MouseButtonChanged(buttonIndex, down)
}
}
}
func (app *Application) initGuiSizes() {
if app.GuiScale < 0.5 {
app.GuiScale = 1.0
} else if app.GuiScale > 10.0 {
app.GuiScale = 10.0
}
if app.FontSize <= 0 {
app.FontSize = 16.0
}
app.FontSize *= app.GuiScale
}
func (app *Application) initGuiStyle() {
if len(app.FontFile) == 0 {
imgui.CurrentIO().SetFontGlobalScale(app.GuiScale)
}
imgui.CurrentStyle().ScaleAllSizes(app.GuiScale)
color := func(r, g, b byte, alpha float32) imgui.Vec4 {
return imgui.Vec4{X: float32(r) / 255.0, Y: float32(g) / 255.0, Z: float32(b) / 255.0, W: alpha}
}
colorDoubleFull := func(alpha float32) imgui.Vec4 { return color(0xC4, 0x38, 0x9F, alpha) }
colorDoubleDark := func(alpha float32) imgui.Vec4 { return color(0x31, 0x01, 0x38, alpha) }
colorTripleFull := func(alpha float32) imgui.Vec4 { return color(0x21, 0xFF, 0x43, alpha) }
colorTripleDark := func(alpha float32) imgui.Vec4 { return color(0x06, 0xCC, 0x94, alpha) }
colorTripleLight := func(alpha float32) imgui.Vec4 { return color(0x51, 0x99, 0x58, alpha) }
style := imgui.CurrentStyle()
style.SetColor(imgui.StyleColorText, colorTripleFull(1.0))
style.SetColor(imgui.StyleColorTextDisabled, colorTripleDark(1.0))
style.SetColor(imgui.StyleColorWindowBg, colorDoubleDark(0.80))
style.SetColor(imgui.StyleColorPopupBg, colorDoubleDark(0.75))
style.SetColor(imgui.StyleColorTitleBgActive, colorTripleLight(1.0))
style.SetColor(imgui.StyleColorFrameBg, colorTripleLight(0.54))
style.SetColor(imgui.StyleColorFrameBgHovered, colorTripleDark(0.4))
style.SetColor(imgui.StyleColorFrameBgActive, colorTripleDark(0.67))
style.SetColor(imgui.StyleColorCheckMark, colorTripleDark(1.0))
style.SetColor(imgui.StyleColorSliderGrabActive, colorTripleDark(1.0))
style.SetColor(imgui.StyleColorButton, colorTripleDark(0.4))
style.SetColor(imgui.StyleColorButtonHovered, colorTripleDark(1.0))
style.SetColor(imgui.StyleColorHeader, colorTripleLight(0.70))
style.SetColor(imgui.StyleColorHeaderHovered, colorTripleDark(0.8))
style.SetColor(imgui.StyleColorHeaderActive, colorTripleDark(1.0))
style.SetColor(imgui.StyleColorResizeGrip, colorTripleDark(0.25))
style.SetColor(imgui.StyleColorResizeGripHovered, colorTripleDark(0.67))
style.SetColor(imgui.StyleColorResizeGripActive, colorTripleDark(0.95))
style.SetColor(imgui.StyleColorTextSelectedBg, colorTripleDark(0.35))
style.SetColor(imgui.StyleColorTab, colorTripleLight(0.54))
style.SetColor(imgui.StyleColorTabHovered, colorTripleLight(0.75))
style.SetColor(imgui.StyleColorTabActive, colorTripleLight(1.0))
style.SetColor(imgui.StyleColorSliderGrab, colorDoubleFull(1.0))
style.SetColor(imgui.StyleColorButtonActive, colorDoubleFull(1.0))
style.SetColor(imgui.StyleColorSeparatorHovered, colorDoubleFull(0.78))
style.SetColor(imgui.StyleColorSeparatorActive, colorTripleLight(1.0))
}
func (app *Application) initSignalling() {
app.cmdStack = new(cmd.Stack)
app.txnBuilder.Commander = app
}
func (app *Application) initModel() {
app.mod = world.NewMod(app.resourcesChanged, app.modReset)
app.cp = text.DefaultCodepage()
app.textLineCache = text.NewLineCache(app.cp, app.mod)
app.textPageCache = text.NewPageCache(app.cp, app.mod)
app.messagesCache = text.NewElectronicMessageCache(app.cp, app.mod)
app.movieCache = movie.NewCache(app.cp, app.mod)
app.soundEffectCache = sound.NewEffectCache(app.mod)
app.levels = edit.NewEditableLevels(&app.txnBuilder, app.mod)
app.levelSelection = edit.NewLevelSelectionService(app.levels)
app.gameObjectsService = edit.NewGameObjectsService(app.mod)
app.levelEditorService = edit.NewLevelEditorService(&app.txnBuilder, app.gameObjectsService, app.levels, app.levelSelection)
app.paletteCache = graphics.NewPaletteCache(app.gl, app.mod)
app.textureCache = graphics.NewTextureCache(app.gl, app.mod)
app.frameCache = graphics.NewFrameCache(app.gl)
app.animationCache = bitmap.NewAnimationCache(app.mod)
}
func (app *Application) resourcesChanged(modifiedIDs []resource.ID, failedIDs []resource.ID) {
app.textLineCache.InvalidateResources(modifiedIDs)
app.textPageCache.InvalidateResources(modifiedIDs)
app.messagesCache.InvalidateResources(modifiedIDs)
app.movieCache.InvalidateResources(modifiedIDs)
app.soundEffectCache.InvalidateResources(modifiedIDs)
app.levels.InvalidateResources(modifiedIDs)
app.paletteCache.InvalidateResources(modifiedIDs)
app.textureCache.InvalidateResources(modifiedIDs)
app.animationCache.InvalidateResources(modifiedIDs)
}
func (app *Application) modReset() {
app.cmdStack = new(cmd.Stack)
}
// nolint: lll
func (app *Application) initView() {
textViewer := media.NewTextViewerService(app.textLineCache, app.textPageCache, app.mod)
textSetter := media.NewTextSetterService(app.cp)
audioViewer := media.NewAudioViewerService(app.movieCache, app.mod)
audioSetter := media.NewAudioSetterService()
movieViewer := media.NewMovieViewerService(app.movieCache, app.mod)
movieSetter := media.NewMovieSetterService(app.cp)
soundEffectViewer := media.NewSoundViewerService(app.soundEffectCache, app.mod)
soundEffectSetter := media.NewSoundSetterService()
soundEffectService := undoable.NewSoundEffectService(edit.NewSoundEffectService(soundEffectViewer, soundEffectSetter), app)
augmentedTextService := undoable.NewAugmentedTextService(edit.NewAugmentedTextService(textViewer, textSetter, audioViewer, audioSetter), app)
movieService := undoable.NewMovieService(edit.NewMovieService(app.cp, movieViewer, movieSetter), app)
app.projectService = edit.NewProjectService(&app.txnBuilder, app.mod)
app.gameStateService = edit.NewGameStateService(&app.txnBuilder)
app.projectView = project.NewView(app.projectService, &app.modalState, app.GuiScale, &app.txnBuilder)
app.archiveView = archives.NewArchiveView(&app.txnBuilder, app.gameStateService, app.mod, app.textLineCache, app.cp, &app.modalState, app.GuiScale, app)
app.levelControlView = levels.NewControlView(app.levels, app.levelSelection, app.levelEditorService, app.GuiScale, app.textLineCache, app.textureCache, &app.txnBuilder)
app.levelTilesView = levels.NewTilesView(app.levelEditorService, app.GuiScale, app.textLineCache, app.textureCache, &app.txnBuilder)
app.levelObjectsView = levels.NewObjectsView(app.gameObjectsService, app.levelEditorService, app.levelSelection, app.gameStateService, app.GuiScale, app.textLineCache, app.textureCache, &app.txnBuilder, app.gl)
app.messagesView = messages.NewMessagesView(app.mod, app.messagesCache, app.cp, app.movieCache, app.textureCache, &app.modalState, app.clipboard, app.GuiScale, app)
app.textsView = texts.NewTextsView(augmentedTextService, &app.modalState, app.clipboard, app.GuiScale)
app.bitmapsView = bitmaps.NewBitmapsView(app.mod, app.textureCache, app.paletteCache, &app.modalState, app.clipboard, app.GuiScale, app)
app.texturesView = textures.NewTexturesView(app.mod, app.textLineCache, app.cp, app.textureCache, app.paletteCache, &app.modalState, app.clipboard, app.GuiScale, app)
app.animationsView = animations.NewAnimationsView(app.mod, app.textureCache, app.paletteCache, app.animationCache, &app.modalState, app.GuiScale, app)
app.moviesView = movies.NewMoviesView(app.mod, app.frameCache, movieService, &app.modalState, app.GuiScale, app)
app.soundEffectsView = sounds.NewSoundEffectsView(soundEffectService, &app.modalState, app.GuiScale)
app.objectsView = objects.NewView(app.mod, app.textLineCache, app.cp, app.textureCache, app.paletteCache, &app.modalState, app.clipboard, app.GuiScale, app)
app.aboutView = about.NewView(app.clipboard, app.GuiScale, app.Version)
app.licensesView = about.NewLicensesView(app.GuiScale)
}
// Queue requests to perform the given command.
func (app *Application) Queue(command cmd.Command) {
err := app.projectService.ModifyModWith(func(modder world.Modder) error {
return app.cmdStack.Perform(command, modder)
})
if err != nil {
app.onFailure("command", "", err)
}
}
func (app *Application) onFailure(source string, details string, err error) {
app.failurePending = true
app.failureMessage = fmt.Sprintf("Source: %v\nDetails: %v\nError: %v", source, details, err)
}
func (app *Application) renderMainMenu() {
windowEntry := func(name string, shortcut string, isOpen *bool) {
if imgui.MenuItemV(name, shortcut, *isOpen, true) {
*isOpen = !*isOpen
}
}
if imgui.BeginMainMenuBar() {
if imgui.BeginMenu("File") {
if imgui.MenuItemV("New Project", "Ctrl+N", false, true) {
app.newProject()
}
if imgui.MenuItemV("Load Project", "", false, true) {
app.loadProject()
}
if imgui.MenuItemV("Save Project", "", false, true) {
app.saveProject()
}
imgui.Separator()
if imgui.MenuItemV("Save Mod", "Ctrl+S", false, true) {
app.projectView.StartSavingMod()
}
windowEntry("Project", "F1", app.projectView.WindowOpen())
imgui.Separator()
if imgui.MenuItem("Exit") {
app.window.SetCloseRequest(true)
}
imgui.EndMenu()
}
if imgui.BeginMenu("Edit") {
if imgui.MenuItemV("Undo", "Ctrl+Z", false, app.cmdStack.CanUndo()) {
app.tryUndo()
}
if imgui.MenuItemV("Redo", "Ctrl+Y / Ctrl+Shift+Z", false, app.cmdStack.CanRedo()) {
app.tryRedo()
}
imgui.EndMenu()
}
if imgui.BeginMenu("Window") {
windowEntry("Archive", "", app.archiveView.WindowOpen())
windowEntry("Level Control", "F2", app.levelControlView.WindowOpen())
windowEntry("Level Tiles", "F3", app.levelTilesView.WindowOpen())
windowEntry("Level Objects", "F4", app.levelObjectsView.WindowOpen())
windowEntry("Messages", "F5", app.messagesView.WindowOpen())
windowEntry("Texts", "", app.textsView.WindowOpen())
windowEntry("Bitmaps", "", app.bitmapsView.WindowOpen())
windowEntry("Textures", "", app.texturesView.WindowOpen())
windowEntry("Animations", "", app.animationsView.WindowOpen())
windowEntry("Movies", "", app.moviesView.WindowOpen())
windowEntry("Sound Effects", "", app.soundEffectsView.WindowOpen())
windowEntry("Game Objects", "", app.objectsView.WindowOpen())
imgui.EndMenu()
}
if imgui.BeginMenu("Help") {
if imgui.MenuItem("About...") {
app.aboutView.Show()
}
if imgui.MenuItem("Licenses") {
app.licensesView.Show()
}
imgui.EndMenu()
}
imgui.EndMainMenuBar()
}
}
func (app *Application) handleFailure() {
if app.failurePending {
imgui.OpenPopup("Failure Message")
app.failurePending = false
}
if imgui.BeginPopupModal("Failure Message") {
imgui.Text(`Something went wrong. This is bad and I am sorry.
You have the option to "Ignore" this and hope for the best.
This action also clears the undo/redo buffer.
Or you can simply "Exit" the application and then restart it.
This action loses any pending changes.
Perhaps you can make something with the details of the error below.
If you can reproduce this, please make a screenshot and
report it with details on how to reproduce it on the
http://www.systemshock.org forums. Thank you!
`)
imgui.Text("Version: " + app.Version)
imgui.Separator()
imgui.Text(app.failureMessage)
imgui.Separator()
if imgui.Button("Ignore") {
app.failureMessage = ""
app.cmdStack = new(cmd.Stack)
imgui.CloseCurrentPopup()
}
imgui.SameLine()
if imgui.Button("Exit") {
app.window.SetCloseRequest(true)
}
imgui.EndPopup()
}
}
const settingsFileExtension = "hacked-project"
func projectFileTypes() []external.TypeInfo {
return []external.TypeInfo{
{
Title: "HackEd Project File (*." + settingsFileExtension + ")",
Extensions: []string{settingsFileExtension},
},
}
}
func (app *Application) newProject() {
app.saveStoredProject()
app.restoreProjectState(projectState{}, "")
}
func (app *Application) loadProject() {
app.saveStoredProject()
external.LoadFile(&app.modalState, projectFileTypes(), func(filename string) error {
state, err := projectStateFromFile(filename)
if err != nil {
return err
}
app.restoreProjectState(state, filename)
return nil
})
}
func (app *Application) saveStoredProject() {
currentFilename := app.projectService.CurrentStateFilename()
if len(currentFilename) > 0 {
currentState := app.currentProjectState()
_ = currentState.SaveTo(currentFilename)
}
}
func (app *Application) saveProject() {
currentState := app.currentProjectState()
currentFilename := app.projectService.CurrentStateFilename()
if (len(currentFilename) == 0) || (currentState.SaveTo(currentFilename) != nil) {
external.SaveFile(&app.modalState, projectFileTypes(), func(filename string) error {
completeFilename := filename
dotExtension := "." + settingsFileExtension
if !strings.HasSuffix(completeFilename, dotExtension) {
completeFilename += dotExtension
}
err := currentState.SaveTo(completeFilename)
if err != nil {
return err
}
app.projectService.SetCurrentStateFilename(completeFilename)
app.window.SetTitleSuffix(completeFilename)
return nil
})
}
}
func (app *Application) currentProjectState() projectState {
projectSettings := app.projectService.CurrentSettings()
gameStateSettings := app.gameStateService.CurrentSettings()
windowOpenByName := app.windowOpenByName()
var openWindows []string
for key, open := range windowOpenByName {
if *open {
openWindows = append(openWindows, key)
}
}
activeLevelIndex := app.levelSelection.CurrentLevelID()
return projectState{
ProjectSettings: &projectSettings,
GameStateSettings: &gameStateSettings,
OpenWindows: openWindows,
ActiveLevelIndex: &activeLevelIndex,
}
}
func (app *Application) saveWorkspace() {
currentProjectState := app.currentProjectState()
currentProjectStateFilename := app.projectService.CurrentStateFilename()
if len(currentProjectStateFilename) > 0 {
err := currentProjectState.SaveTo(currentProjectStateFilename)
if err != nil {
currentProjectStateFilename = ""
} else {
currentProjectState = projectState{}
}
}
lastProjectState := lastProjectState{
projectState: currentProjectState,
LastProject: currentProjectStateFilename,
}
_ = lastProjectState.SaveTo(app.lastProjectStateConfigFilename())
}
func (app *Application) restoreWorkspace() {
lastProjectState := lastProjectStateFromFile(app.lastProjectStateConfigFilename())
projectStateToRestore := lastProjectState.projectState
projectStateFilename := ""
if len(lastProjectState.LastProject) > 0 {
state, err := projectStateFromFile(lastProjectState.LastProject)
if err == nil {
projectStateToRestore = state
projectStateFilename = lastProjectState.LastProject
}
}
app.restoreProjectState(projectStateToRestore, projectStateFilename)
}
func (app *Application) restoreProjectState(state projectState, filename string) {
var projectSettings edit.ProjectSettings
if state.ProjectSettings != nil {
projectSettings = *state.ProjectSettings
}
app.projectService.RestoreProject(projectSettings, filename)
var gameStateSettings edit.GameStateSettings
if state.GameStateSettings != nil {
gameStateSettings = *state.GameStateSettings
}
app.gameStateService.RestoreSettings(gameStateSettings)
windowOpenByName := app.windowOpenByName()
for _, open := range windowOpenByName {
*open = false
}
for _, key := range state.OpenWindows {
open := windowOpenByName[key]
if open != nil {
*open = true
}
}
if len(state.OpenWindows) == 0 {
*app.projectView.WindowOpen() = true
}
activeLevel := world.StartingLevel
if (state.ActiveLevelIndex != nil) && app.levels.IsLevelAvailable(*state.ActiveLevelIndex) {
activeLevel = *state.ActiveLevelIndex
}
app.levelSelection.SetCurrentLevelID(activeLevel)
app.window.SetTitleSuffix(filename)
}
func (app *Application) updateAutoSave() {
status := app.projectService.SaveStatus()
app.window.SetProjectModified(status.FilesModified > 0)
if status.SavePending && (status.SaveIn == 0) {
status.ConfirmPendingSave()
app.projectView.StartSavingMod()
}
}
func (app *Application) windowStateConfigFilename() string {
return filepath.Join(app.ConfigDir, "WindowState.json")
}
func (app *Application) lastProjectStateConfigFilename() string {
return filepath.Join(app.ConfigDir, "LastProjectState.json")
}
func (app *Application) windowOpenByName() map[string]*bool {
return map[string]*bool{
"project": app.projectView.WindowOpen(),
"archive": app.archiveView.WindowOpen(),
"levelControl": app.levelControlView.WindowOpen(),
"levelTiles": app.levelTilesView.WindowOpen(),
"levelObjects": app.levelObjectsView.WindowOpen(),
"messages": app.messagesView.WindowOpen(),
"texts": app.textsView.WindowOpen(),
"bitmaps": app.bitmapsView.WindowOpen(),
"textures": app.texturesView.WindowOpen(),
"animations": app.animationsView.WindowOpen(),
"movies": app.moviesView.WindowOpen(),
"soundEffects": app.soundEffectsView.WindowOpen(),
"gameObjects": app.objectsView.WindowOpen(),
}
}