946 lines
31 KiB
Go
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(),
|
|
}
|
|
}
|