Adding modding folder with a hacked build.
This commit is contained in:
@@ -0,0 +1,945 @@
|
||||
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(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package editor
|
||||
|
||||
import "github.com/inkyblackness/hacked/ui/opengl"
|
||||
|
||||
type clipboardAdapter struct {
|
||||
window opengl.Window
|
||||
}
|
||||
|
||||
func (adapter clipboardAdapter) String() (string, error) {
|
||||
return adapter.window.ClipboardString()
|
||||
}
|
||||
|
||||
func (adapter clipboardAdapter) SetString(value string) {
|
||||
adapter.window.SetClipboardString(value)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package about
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
)
|
||||
|
||||
// LicensesView handles the about display.
|
||||
type LicensesView struct {
|
||||
guiScale float32
|
||||
|
||||
model viewModel
|
||||
}
|
||||
|
||||
// NewLicensesView creates a new instance for the about display.
|
||||
func NewLicensesView(guiScale float32) *LicensesView {
|
||||
return &LicensesView{
|
||||
guiScale: guiScale,
|
||||
|
||||
model: freshViewModel(),
|
||||
}
|
||||
}
|
||||
|
||||
// Show requests to show the licenses view.
|
||||
func (view *LicensesView) Show() {
|
||||
view.model.windowOpen = true
|
||||
}
|
||||
|
||||
// Render requests to render the view.
|
||||
func (view *LicensesView) Render() {
|
||||
if view.model.windowOpen {
|
||||
imgui.OpenPopup("Licenses")
|
||||
view.model.windowOpen = false
|
||||
imgui.SetNextWindowSize(imgui.Vec2{X: 640 * view.guiScale, Y: 480 * view.guiScale})
|
||||
}
|
||||
if imgui.BeginPopupModalV("Licenses", nil, imgui.WindowFlagsHorizontalScrollbar|imgui.WindowFlagsNoSavedSettings) {
|
||||
view.renderContent()
|
||||
imgui.EndPopup()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *LicensesView) renderContent() {
|
||||
if imgui.TreeNodeV("System Shock", imgui.TreeNodeFlagsDefaultOpen) {
|
||||
imgui.Text("System Shock by Night Dive Studios, LLC.\nOriginally by Looking Glass Technologies")
|
||||
imgui.TreePop()
|
||||
}
|
||||
add := func(title, license string) {
|
||||
if imgui.TreeNode(title) {
|
||||
imgui.Text(license)
|
||||
imgui.TreePop()
|
||||
}
|
||||
}
|
||||
|
||||
add("InkyBlackness - HackEd", inkyblacknessHackedLicense)
|
||||
add("InkyBlackness - imgui", inkyblacknessImGuiLicense)
|
||||
add("go-gl/glfw - Go bindings for GLFW 3", goglGlfwLicense)
|
||||
add("go-gl/gl", goglOpenGLLicense)
|
||||
add("go-gl/mathgl", goglMathGLLicense)
|
||||
add("Dear ImGui", dearImGuiLicense)
|
||||
add("GLFW", glfwLicense)
|
||||
add("MinGW-w64", mingwLicense)
|
||||
|
||||
imgui.Separator()
|
||||
if imgui.Button("OK") {
|
||||
imgui.CloseCurrentPopup()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package about
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/external"
|
||||
)
|
||||
|
||||
// View handles the about display.
|
||||
type View struct {
|
||||
version string
|
||||
guiScale float32
|
||||
clipboard external.Clipboard
|
||||
|
||||
model viewModel
|
||||
}
|
||||
|
||||
// NewView creates a new instance for the about display.
|
||||
func NewView(clipboard external.Clipboard, guiScale float32, version string) *View {
|
||||
return &View{
|
||||
version: version,
|
||||
guiScale: guiScale,
|
||||
clipboard: clipboard,
|
||||
|
||||
model: freshViewModel(),
|
||||
}
|
||||
}
|
||||
|
||||
// Show requests to show the about view.
|
||||
func (view *View) Show() {
|
||||
view.model.windowOpen = true
|
||||
}
|
||||
|
||||
// Render requests to render the view.
|
||||
func (view *View) Render() {
|
||||
if view.model.windowOpen {
|
||||
imgui.OpenPopup("About")
|
||||
view.model.windowOpen = false
|
||||
}
|
||||
if imgui.BeginPopupModalV("About", nil, imgui.WindowFlagsAlwaysAutoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsHorizontalScrollbar) {
|
||||
view.renderContent()
|
||||
imgui.EndPopup()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) renderContent() {
|
||||
projectURL := "https://inkyblackness.github.io"
|
||||
communityURL := "https://www.systemshock.org"
|
||||
userguideURL := "https://github.com/inkyblackness/hacked/wiki"
|
||||
urlLine := func(title, url string) {
|
||||
imgui.Text(title + ": " + url)
|
||||
imgui.SameLine()
|
||||
if imgui.Button("-> Clip##" + title) {
|
||||
view.clipboard.SetString(url)
|
||||
}
|
||||
}
|
||||
|
||||
imgui.Text("InkyBlackness - HackEd - " + view.version)
|
||||
imgui.Separator()
|
||||
urlLine("User guide", userguideURL)
|
||||
urlLine("Community", communityURL)
|
||||
urlLine("Project", projectURL)
|
||||
imgui.Separator()
|
||||
imgui.Text("Thanks to: 3RDPlayer: testing, ToxicFrog: name idea.")
|
||||
imgui.Text("Written in Go. Because I can.")
|
||||
imgui.Separator()
|
||||
if imgui.Button("OK") {
|
||||
imgui.CloseCurrentPopup()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package about
|
||||
|
||||
type viewModel struct {
|
||||
windowOpen bool
|
||||
}
|
||||
|
||||
func freshViewModel() viewModel {
|
||||
return viewModel{}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package about
|
||||
|
||||
var inkyblacknessHackedLicense = `New BSD License
|
||||
|
||||
© 2018, Christian Haas
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of "InkyBlackness" nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE LISTED COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
`
|
||||
|
||||
var inkyblacknessImGuiLicense = `New BSD License
|
||||
|
||||
© 2018, Christian Haas
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of "InkyBlackness" nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE LISTED COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
`
|
||||
|
||||
var dearImGuiLicense = `The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2018 Omar Cornut
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
`
|
||||
|
||||
var mingwLicense = `With exception of certain parts that are prominently marked as being
|
||||
in the Public Domain, BSD, or LGPL this Software is provided under the
|
||||
Zope Public License (ZPL) Version 2.1.
|
||||
|
||||
Copyright (c) 2009 - 2013 by the mingw-w64 project
|
||||
|
||||
See the AUTHORS file for the list of contributors to the mingw-w64 project.
|
||||
|
||||
This license has been certified as open source. It has also been designated
|
||||
as GPL compatible by the Free Software Foundation (FSF).
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions in source code must retain the accompanying copyright
|
||||
notice, this list of conditions, and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the accompanying
|
||||
copyright notice, this list of conditions, and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
3. Names of the copyright holders must not be used to endorse or promote
|
||||
products derived from this software without prior written permission
|
||||
from the copyright holders.
|
||||
4. The right to distribute this software or to use it for any purpose does
|
||||
not give you the right to use Servicemarks (sm) or Trademarks (tm) of
|
||||
the copyright holders. Use of them is covered by separate agreement
|
||||
with the copyright holders.
|
||||
5. If any files are modified, you must cause the modified files to carry
|
||||
prominent notices stating that you changed the files and the date of
|
||||
any change.
|
||||
|
||||
Disclaimer
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY EXPRESSED
|
||||
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
||||
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
`
|
||||
|
||||
var goglGlfwLicense = `Copyright (c) 2012 The glfw3-go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
`
|
||||
|
||||
var goglOpenGLLicense = `The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Eric Woroshow
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
`
|
||||
|
||||
var goglMathGLLicense = `Copyright ©2013 The go-gl Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the gonum project nor the names of its authors and
|
||||
contributors may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
`
|
||||
|
||||
var glfwLicense = `Copyright (c) 2002-2006 Marcus Geelnard
|
||||
Copyright (c) 2006-2016 Camilla Löwy <elmindreda@glfw.org>
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would
|
||||
be appreciated but is not required.
|
||||
|
||||
2. Altered source versions must be plainly marked as such, and must not
|
||||
be misrepresented as being the original software.
|
||||
|
||||
3. This notice may not be removed or altered from any source
|
||||
distribution.
|
||||
`
|
||||
@@ -0,0 +1,41 @@
|
||||
package animations
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
)
|
||||
|
||||
type setAnimationCommand struct {
|
||||
model *viewModel
|
||||
|
||||
animationKey resource.Key
|
||||
framesID resource.ID
|
||||
|
||||
oldAnimation []byte
|
||||
newAnimation []byte
|
||||
|
||||
oldFrames [][]byte
|
||||
newFrames [][]byte
|
||||
}
|
||||
|
||||
func (command setAnimationCommand) Do(modder world.Modder) error {
|
||||
return command.perform(modder, command.newAnimation, command.newFrames)
|
||||
}
|
||||
|
||||
func (command setAnimationCommand) Undo(modder world.Modder) error {
|
||||
return command.perform(modder, command.oldAnimation, command.oldFrames)
|
||||
}
|
||||
|
||||
func (command setAnimationCommand) perform(modder world.Modder, animData []byte, frames [][]byte) error {
|
||||
if len(frames) == 0 {
|
||||
modder.DelResource(command.animationKey.Lang, command.animationKey.ID.Plus(command.animationKey.Index))
|
||||
modder.DelResource(command.animationKey.Lang, command.framesID)
|
||||
} else {
|
||||
modder.SetResourceBlock(command.animationKey.Lang, command.animationKey.ID.Plus(command.animationKey.Index), 0, animData)
|
||||
modder.SetResourceBlocks(command.animationKey.Lang, command.framesID, frames)
|
||||
}
|
||||
|
||||
command.model.restoreFocus = true
|
||||
command.model.currentKey = command.animationKey
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package animations
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/external"
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/serial/rle"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
type animationInfo struct {
|
||||
title string
|
||||
}
|
||||
|
||||
var knownAnimationTypes = map[resource.ID]animationInfo{
|
||||
ids.VideoMailAnimationsStart: {title: "Video Mail Parts"},
|
||||
}
|
||||
|
||||
var knownAnimationTypesOrder = []resource.ID{
|
||||
ids.VideoMailAnimationsStart,
|
||||
}
|
||||
|
||||
// View provides edit controls for animations.
|
||||
type View struct {
|
||||
mod *world.Mod
|
||||
imageCache *graphics.TextureCache
|
||||
paletteCache *graphics.PaletteCache
|
||||
animationCache *bitmap.AnimationCache
|
||||
|
||||
modalStateMachine gui.ModalStateMachine
|
||||
guiScale float32
|
||||
commander cmd.Commander
|
||||
|
||||
model viewModel
|
||||
}
|
||||
|
||||
// NewAnimationsView returns a new instance.
|
||||
func NewAnimationsView(mod *world.Mod, imageCache *graphics.TextureCache, paletteCache *graphics.PaletteCache,
|
||||
animationCache *bitmap.AnimationCache,
|
||||
modalStateMachine gui.ModalStateMachine, guiScale float32, commander cmd.Commander) *View {
|
||||
view := &View{
|
||||
mod: mod,
|
||||
imageCache: imageCache,
|
||||
paletteCache: paletteCache,
|
||||
animationCache: animationCache,
|
||||
|
||||
modalStateMachine: modalStateMachine,
|
||||
guiScale: guiScale,
|
||||
commander: commander,
|
||||
|
||||
model: freshViewModel(),
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// WindowOpen returns the flag address, to be used with the main menu.
|
||||
func (view *View) WindowOpen() *bool {
|
||||
return &view.model.windowOpen
|
||||
}
|
||||
|
||||
// Render renders the view.
|
||||
func (view *View) Render() {
|
||||
if view.model.restoreFocus {
|
||||
imgui.SetNextWindowFocus()
|
||||
view.model.restoreFocus = false
|
||||
view.model.windowOpen = true
|
||||
}
|
||||
if view.model.windowOpen {
|
||||
imgui.SetNextWindowSizeV(imgui.Vec2{X: 800 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||
if imgui.BeginV("Animations", view.WindowOpen(), imgui.WindowFlagsNoCollapse|imgui.WindowFlagsHorizontalScrollbar) {
|
||||
view.renderContent()
|
||||
}
|
||||
imgui.End()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) renderContent() {
|
||||
if imgui.BeginChildV("Properties", imgui.Vec2{X: 350 * view.guiScale, Y: 0}, false, 0) {
|
||||
imgui.PushItemWidth(-150 * view.guiScale)
|
||||
if imgui.BeginCombo("Animation Type", knownAnimationTypes[view.model.currentKey.ID].title) {
|
||||
for _, id := range knownAnimationTypesOrder {
|
||||
if imgui.SelectableV(knownAnimationTypes[id].title, id == view.model.currentKey.ID, 0, imgui.Vec2{}) {
|
||||
view.model.currentKey.ID = id
|
||||
view.model.currentKey.Index = 0
|
||||
view.model.currentFrame = 0
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
|
||||
info, _ := ids.Info(view.model.currentKey.ID)
|
||||
|
||||
if gui.StepSliderInt("Index", &view.model.currentKey.Index, 0, info.MaxCount-1) {
|
||||
view.model.currentFrame = 0
|
||||
}
|
||||
|
||||
imgui.Separator()
|
||||
|
||||
view.renderProperties()
|
||||
|
||||
imgui.PopItemWidth()
|
||||
}
|
||||
imgui.EndChild()
|
||||
imgui.SameLine()
|
||||
|
||||
if imgui.BeginChildV("Frames", imgui.Vec2{X: -1, Y: 0}, false, 0) {
|
||||
anim, hasAnim, _ := view.currentAnimation()
|
||||
|
||||
if hasAnim {
|
||||
frameKey := resource.KeyOf(anim.ResourceID, resource.LangAny, view.model.currentFrame)
|
||||
if view.cacheFrame(frameKey) {
|
||||
render.TextureImage("Frame", view.imageCache, frameKey,
|
||||
imgui.Vec2{X: float32(anim.Width) * view.guiScale, Y: float32(anim.Height) * view.guiScale})
|
||||
if imgui.Button("Export") {
|
||||
view.requestExport()
|
||||
}
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Import") {
|
||||
view.requestImport()
|
||||
}
|
||||
}
|
||||
}
|
||||
imgui.EndChild()
|
||||
}
|
||||
|
||||
func (view *View) cacheFrame(key resource.Key) bool {
|
||||
var err error
|
||||
var lastKey *resource.Key
|
||||
for index := 0; (index <= key.Index) && (err == nil); index++ {
|
||||
nextKey := resource.KeyOf(key.ID, key.Lang, index)
|
||||
_, err = view.imageCache.TextureReferenced(nextKey, lastKey)
|
||||
lastKey = &nextKey
|
||||
}
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (view *View) renderProperties() {
|
||||
anim, hasAnim, _ := view.currentAnimation()
|
||||
widthString := ""
|
||||
heightString := ""
|
||||
lastFrame := 0
|
||||
|
||||
if hasAnim {
|
||||
widthString = fmt.Sprintf("%d", anim.Width)
|
||||
heightString = fmt.Sprintf("%d", anim.Height)
|
||||
for _, entry := range anim.Entries {
|
||||
lastFrame = int(entry.LastFrame)
|
||||
}
|
||||
}
|
||||
|
||||
imgui.LabelText("Width", widthString)
|
||||
imgui.LabelText("Height", heightString)
|
||||
|
||||
gui.StepSliderInt("Frame Index", &view.model.currentFrame, 0, lastFrame)
|
||||
}
|
||||
|
||||
func (view *View) currentAnimation() (bitmap.Animation, bool, bool) {
|
||||
key := resource.KeyOf(view.model.currentKey.ID.Plus(view.model.currentKey.Index), resource.LangAny, 0)
|
||||
anim, err := view.animationCache.Animation(key)
|
||||
if err != nil {
|
||||
return anim, false, true
|
||||
}
|
||||
readOnly := len(view.mod.ModifiedBlocks(resource.LangAny, key.ID)) == 0
|
||||
return anim, true, readOnly
|
||||
}
|
||||
|
||||
func (view *View) requestImport() {
|
||||
info := "File must be an animated GIF file.\nIdeally, it matches the game palette 1:1,\nothers are mapped closest fitting."
|
||||
types := []external.TypeInfo{{Title: "Animation files (*.gif)", Extensions: []string{"gif"}}}
|
||||
var fileHandler func(string)
|
||||
|
||||
fileHandler = func(filename string) {
|
||||
reader, err := os.Open(filename)
|
||||
if err != nil {
|
||||
external.Import(view.modalStateMachine, "Could not open file.\n"+info, types, fileHandler, true)
|
||||
return
|
||||
}
|
||||
defer func() { _ = reader.Close() }()
|
||||
data, err := gif.DecodeAll(reader)
|
||||
if err != nil {
|
||||
external.Import(view.modalStateMachine, "File not recognized as GIF.\n"+info, types, fileHandler, true)
|
||||
return
|
||||
}
|
||||
|
||||
palette, err := view.paletteCache.Palette(0)
|
||||
if err != nil {
|
||||
external.Import(view.modalStateMachine, "Can not import image without having a palette loaded.\n"+info, types, fileHandler, true)
|
||||
return
|
||||
}
|
||||
anim := bitmap.Animation{
|
||||
Width: int16(data.Config.Width),
|
||||
Height: int16(data.Config.Height),
|
||||
ResourceID: view.model.currentKey.ID.Plus(view.model.currentKey.Index).Plus(-12),
|
||||
IntroFlag: 0,
|
||||
}
|
||||
var frames [][]byte
|
||||
rawPalette := palette.Palette()
|
||||
|
||||
highestBitShift := func(value int16) (result byte) {
|
||||
if value != 0 {
|
||||
for (value >> result) != 1 {
|
||||
result++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var prevFrame []byte
|
||||
for index, img := range data.Image {
|
||||
if (img.Bounds().Max.X == data.Config.Width) && (img.Bounds().Max.Y == data.Config.Height) {
|
||||
bitmapper := bitmap.NewBitmapper(&rawPalette)
|
||||
bmp := bitmapper.Map(img)
|
||||
entry := bitmap.AnimationEntry{
|
||||
FirstFrame: byte(index),
|
||||
LastFrame: byte(index),
|
||||
FrameTime: int16(data.Delay[index] * 10),
|
||||
}
|
||||
anim.Entries = append(anim.Entries, entry)
|
||||
bmp.Header.Type = bitmap.TypeCompressed8Bit
|
||||
bmp.Header.WidthFactor = highestBitShift(bmp.Header.Width)
|
||||
bmp.Header.HeightFactor = highestBitShift(bmp.Header.Height)
|
||||
bmp.Header.Area = [4]int16{0, 0, anim.Width, anim.Height}
|
||||
bmp.Header.Stride = uint16(bmp.Header.Width)
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
_ = binary.Write(buf, binary.LittleEndian, &bmp.Header)
|
||||
_ = rle.Compress(buf, bmp.Pixels, prevFrame)
|
||||
prevFrame = bmp.Pixels
|
||||
frames = append(frames, buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
view.requestSetAnimation(anim, frames)
|
||||
}
|
||||
|
||||
external.Import(view.modalStateMachine, info, types, fileHandler, false)
|
||||
}
|
||||
|
||||
func (view *View) requestExport() {
|
||||
anim, hasAnim, _ := view.currentAnimation()
|
||||
if hasAnim {
|
||||
filename := fmt.Sprintf("Anim%04X.gif", int(view.model.currentKey.ID.Plus(view.model.currentKey.Index)))
|
||||
view.exportTo(filename, anim)
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) exportTo(filename string, anim bitmap.Animation) {
|
||||
info := "File to be written: " + filename
|
||||
var exportTo func(string)
|
||||
|
||||
exportTo = func(dirname string) {
|
||||
writer, err := os.Create(filepath.Join(dirname, filename))
|
||||
if err != nil {
|
||||
external.Export(view.modalStateMachine, "Could not create file.\n"+info, exportTo, true)
|
||||
return
|
||||
}
|
||||
defer func() { _ = writer.Close() }()
|
||||
|
||||
palTex, err := view.paletteCache.Palette(0)
|
||||
if err != nil {
|
||||
external.Export(view.modalStateMachine, "Could not create file. No palette loaded.\n"+info, exportTo, true)
|
||||
return
|
||||
}
|
||||
|
||||
colorPalette := palTex.Palette().ColorPalette(false)
|
||||
data := gif.GIF{
|
||||
Config: image.Config{
|
||||
Width: int(anim.Width),
|
||||
Height: int(anim.Height),
|
||||
ColorModel: colorPalette,
|
||||
},
|
||||
LoopCount: -1,
|
||||
}
|
||||
|
||||
imageRect := image.Rect(0, 0, data.Config.Width, data.Config.Height)
|
||||
frameIndex := 0
|
||||
for _, entry := range anim.Entries {
|
||||
for frameIndex <= int(entry.LastFrame) {
|
||||
frameKey := resource.KeyOf(anim.ResourceID, resource.LangAny, frameIndex)
|
||||
if !view.cacheFrame(frameKey) {
|
||||
external.Export(view.modalStateMachine, "Failed to cache frame.\n"+info, exportTo, true)
|
||||
return
|
||||
}
|
||||
frameTex, _ := view.imageCache.Texture(frameKey)
|
||||
frameImg := image.NewPaletted(imageRect, colorPalette)
|
||||
frameImg.Pix = frameTex.PixelData()
|
||||
data.Image = append(data.Image, frameImg)
|
||||
data.Delay = append(data.Delay, int(entry.FrameTime)/10)
|
||||
frameIndex++
|
||||
}
|
||||
}
|
||||
|
||||
err = gif.EncodeAll(writer, &data)
|
||||
if err != nil {
|
||||
external.Export(view.modalStateMachine, info, exportTo, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
external.Export(view.modalStateMachine, info, exportTo, false)
|
||||
}
|
||||
|
||||
func (view *View) requestSetAnimation(newAnim bitmap.Animation, newFrames [][]byte) {
|
||||
encodeAnim := func(anim bitmap.Animation) []byte {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
_ = bitmap.WriteAnimation(buf, anim)
|
||||
return buf.Bytes()
|
||||
}
|
||||
command := setAnimationCommand{
|
||||
model: &view.model,
|
||||
animationKey: view.model.currentKey,
|
||||
newAnimation: encodeAnim(newAnim),
|
||||
newFrames: newFrames,
|
||||
framesID: newAnim.ResourceID,
|
||||
}
|
||||
oldAnim, oldExisting, _ := view.currentAnimation()
|
||||
if oldExisting {
|
||||
command.oldAnimation = encodeAnim(oldAnim)
|
||||
}
|
||||
command.oldFrames = view.mod.ModifiedBlocks(view.model.currentKey.Lang, newAnim.ResourceID)
|
||||
view.commander.Queue(command)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package animations
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
type viewModel struct {
|
||||
windowOpen bool
|
||||
restoreFocus bool
|
||||
|
||||
currentKey resource.Key
|
||||
currentFrame int
|
||||
}
|
||||
|
||||
func freshViewModel() viewModel {
|
||||
return viewModel{
|
||||
currentKey: resource.KeyOf(ids.VideoMailAnimationsStart, resource.LangAny, 0),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package archives
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/values"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive"
|
||||
"github.com/inkyblackness/hacked/ss1/edit"
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
type editBooleanVariableDialog struct {
|
||||
machine gui.ModalStateMachine
|
||||
view *View
|
||||
|
||||
service *edit.GameStateService
|
||||
index int
|
||||
varOverride bool
|
||||
info archive.GameVariableInfo
|
||||
|
||||
opened bool
|
||||
}
|
||||
|
||||
func (dialog *editBooleanVariableDialog) Render() {
|
||||
if !dialog.opened {
|
||||
dialog.opened = true
|
||||
dialog.varOverride = dialog.service.BooleanVariableOverride(dialog.index)
|
||||
dialog.info = dialog.service.BooleanVariable(dialog.index)
|
||||
imgui.OpenPopup("Edit boolean variable")
|
||||
}
|
||||
|
||||
imgui.SetNextWindowSize(imgui.Vec2{X: 600, Y: 0}.Times(dialog.view.guiScale))
|
||||
if imgui.BeginPopupModalV("Edit boolean variable", nil,
|
||||
imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||
dialog.renderControls()
|
||||
imgui.Separator()
|
||||
if imgui.Button("OK") {
|
||||
dialog.applyChanges()
|
||||
dialog.close()
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Cancel") {
|
||||
dialog.close()
|
||||
}
|
||||
imgui.EndPopup()
|
||||
} else {
|
||||
dialog.machine.SetState(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (dialog *editBooleanVariableDialog) applyChanges() {
|
||||
if dialog.varOverride {
|
||||
_ = dialog.service.SetBooleanVariable(dialog.index, dialog.info)
|
||||
} else {
|
||||
_ = dialog.service.DefaultBooleanVariable(dialog.index)
|
||||
}
|
||||
}
|
||||
|
||||
func (dialog *editBooleanVariableDialog) close() {
|
||||
dialog.machine.SetState(nil)
|
||||
imgui.CloseCurrentPopup()
|
||||
}
|
||||
|
||||
func (dialog *editBooleanVariableDialog) renderControls() {
|
||||
imgui.PushItemWidth(-150 * dialog.view.guiScale)
|
||||
values.RenderUnifiedCheckboxCombo(false, "Override", values.UnifierFor(dialog.varOverride), func(newValue bool) {
|
||||
dialog.varOverride = newValue
|
||||
})
|
||||
|
||||
textFlags := imgui.InputTextFlagsNoUndoRedo
|
||||
if !dialog.varOverride {
|
||||
textFlags |= imgui.InputTextFlagsReadOnly
|
||||
}
|
||||
imgui.InputTextV("Name", &dialog.info.Name, textFlags, nil)
|
||||
imgui.InputTextMultilineV("Description", &dialog.info.Description, imgui.Vec2{X: 0, Y: 100 * dialog.view.guiScale},
|
||||
textFlags, nil)
|
||||
|
||||
imgui.Separator()
|
||||
defaultValues := map[int]string{0: "False", 1: "True"}
|
||||
valueLabels := map[bool]string{false: "Default", true: "Special"}
|
||||
hasSpecialValues := dialog.info.ValueNames != nil
|
||||
if dialog.varOverride {
|
||||
if imgui.BeginCombo("Values", valueLabels[hasSpecialValues]) {
|
||||
if imgui.SelectableV(valueLabels[false], !hasSpecialValues, 0, imgui.Vec2{}) {
|
||||
dialog.info.ValueNames = nil
|
||||
}
|
||||
if imgui.SelectableV(valueLabels[true], hasSpecialValues, 0, imgui.Vec2{}) {
|
||||
dialog.info.ValueNames = map[int16]string{0: defaultValues[0], 1: defaultValues[1]}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
} else {
|
||||
imgui.LabelText("Values", valueLabels[hasSpecialValues])
|
||||
}
|
||||
hasSpecialValues = dialog.info.ValueNames != nil
|
||||
if hasSpecialValues {
|
||||
for i := int16(0); i < 2; i++ {
|
||||
text := dialog.info.ValueNames[i]
|
||||
if imgui.InputTextV(defaultValues[int(i)], &text, textFlags, nil) {
|
||||
dialog.info.ValueNames[i] = text
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imgui.LabelText(defaultValues[0], defaultValues[0])
|
||||
imgui.LabelText(defaultValues[1], defaultValues[1])
|
||||
}
|
||||
|
||||
initUnifier := values.UnifierFor(dialog.info.ResetValueInt())
|
||||
values.RenderUnifiedCombo(dialog.info.Hardcoded || !dialog.varOverride, "Reset Value", initUnifier,
|
||||
func(u values.Unifier) int { return int(u.Unified().(int16)) },
|
||||
func(value int) string {
|
||||
name, found := dialog.info.ValueNames[int16(value)]
|
||||
if found {
|
||||
return name
|
||||
}
|
||||
return defaultValues[value]
|
||||
},
|
||||
2,
|
||||
func(newValue int) {
|
||||
value16 := int16(newValue)
|
||||
dialog.info.InitValue = &value16
|
||||
})
|
||||
|
||||
imgui.PopItemWidth()
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package archives
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/values"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive"
|
||||
"github.com/inkyblackness/hacked/ss1/edit"
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
type editIntegerVariableDialog struct {
|
||||
machine gui.ModalStateMachine
|
||||
view *View
|
||||
|
||||
service *edit.GameStateService
|
||||
index int
|
||||
varOverride bool
|
||||
info archive.GameVariableInfo
|
||||
|
||||
selectedEnum int
|
||||
opened bool
|
||||
}
|
||||
|
||||
func (dialog *editIntegerVariableDialog) Render() {
|
||||
if !dialog.opened {
|
||||
dialog.opened = true
|
||||
dialog.varOverride = dialog.service.IntegerVariableOverride(dialog.index)
|
||||
dialog.info = dialog.service.IntegerVariable(dialog.index)
|
||||
imgui.OpenPopup("Edit integer variable")
|
||||
}
|
||||
|
||||
imgui.SetNextWindowSize(imgui.Vec2{X: 600, Y: 0}.Times(dialog.view.guiScale))
|
||||
if imgui.BeginPopupModalV("Edit integer variable", nil,
|
||||
imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||
dialog.renderControls()
|
||||
imgui.Separator()
|
||||
if imgui.Button("OK") {
|
||||
dialog.applyChanges()
|
||||
dialog.close()
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Cancel") {
|
||||
dialog.close()
|
||||
}
|
||||
imgui.EndPopup()
|
||||
} else {
|
||||
dialog.machine.SetState(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (dialog *editIntegerVariableDialog) applyChanges() {
|
||||
if dialog.varOverride {
|
||||
_ = dialog.service.SetIntegerVariable(dialog.index, dialog.info)
|
||||
} else {
|
||||
_ = dialog.service.DefaultIntegerVariable(dialog.index)
|
||||
}
|
||||
}
|
||||
|
||||
func (dialog *editIntegerVariableDialog) close() {
|
||||
dialog.machine.SetState(nil)
|
||||
imgui.CloseCurrentPopup()
|
||||
}
|
||||
|
||||
func (dialog *editIntegerVariableDialog) renderControls() {
|
||||
imgui.PushItemWidth(-150 * dialog.view.guiScale)
|
||||
values.RenderUnifiedCheckboxCombo(false, "Override", values.UnifierFor(dialog.varOverride), func(newValue bool) {
|
||||
dialog.varOverride = newValue
|
||||
})
|
||||
|
||||
textFlags := imgui.InputTextFlagsNoUndoRedo
|
||||
if !dialog.varOverride {
|
||||
textFlags |= imgui.InputTextFlagsReadOnly
|
||||
}
|
||||
imgui.InputTextV("Name", &dialog.info.Name, textFlags, nil)
|
||||
imgui.InputTextMultilineV("Description", &dialog.info.Description, imgui.Vec2{X: 0, Y: 100 * dialog.view.guiScale},
|
||||
textFlags, nil)
|
||||
|
||||
imgui.Separator()
|
||||
|
||||
valueLabels := map[bool]string{false: "Numeric", true: "Enumerated"}
|
||||
isEnumerated := len(dialog.info.ValueNames) > 0
|
||||
if imgui.BeginCombo("Values", valueLabels[isEnumerated]) {
|
||||
if imgui.SelectableV(valueLabels[false], !isEnumerated, 0, imgui.Vec2{}) {
|
||||
dialog.info.ValueNames = nil
|
||||
}
|
||||
if imgui.SelectableV(valueLabels[true], isEnumerated, 0, imgui.Vec2{}) {
|
||||
dialog.info.ValueNames = map[int16]string{0: "Zero"}
|
||||
dialog.selectedEnum = 0
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
if len(dialog.info.ValueNames) > 0 {
|
||||
gui.StepSliderInt("Index", &dialog.selectedEnum, math.MinInt16, math.MaxInt16)
|
||||
if dialog.varOverride {
|
||||
text := dialog.info.ValueNames[int16(dialog.selectedEnum)]
|
||||
if imgui.InputTextV("Value", &text, textFlags, nil) {
|
||||
if len(text) > 0 {
|
||||
dialog.info.ValueNames[int16(dialog.selectedEnum)] = text
|
||||
} else {
|
||||
delete(dialog.info.ValueNames, int16(dialog.selectedEnum))
|
||||
if len(dialog.info.ValueNames) == 0 {
|
||||
dialog.info.ValueNames[0] = "Zero"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imgui.LabelText("Value", dialog.info.ValueNames[int16(dialog.selectedEnum)])
|
||||
}
|
||||
|
||||
resetValue := dialog.info.ResetValueInt()
|
||||
enumEntries := toEnumEntries(dialog.info.ValueNames)
|
||||
linearInitIndex := linearIndex(enumEntries, resetValue)
|
||||
initUnifier := values.UnifierFor(linearInitIndex)
|
||||
values.RenderUnifiedCombo(dialog.info.Hardcoded || !dialog.varOverride, "Reset Value", initUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string {
|
||||
if value < 0 {
|
||||
return fmt.Sprintf("%d: (unmapped)", resetValue)
|
||||
}
|
||||
entry := enumEntries[value]
|
||||
return fmt.Sprintf("%d: '%s'", entry.index, entry.value)
|
||||
},
|
||||
len(enumEntries),
|
||||
func(newValue int) {
|
||||
dialog.info.InitValue = &enumEntries[newValue].index
|
||||
})
|
||||
} else {
|
||||
minValue := math.MinInt16
|
||||
maxValue := math.MaxInt16
|
||||
|
||||
if dialog.info.Limits != nil {
|
||||
minValue = int(dialog.info.Limits.Minimum)
|
||||
maxValue = int(dialog.info.Limits.Maximum)
|
||||
}
|
||||
if minValue > maxValue {
|
||||
maxValue = minValue
|
||||
}
|
||||
if dialog.varOverride {
|
||||
if gui.StepSliderInt("Minimum", &minValue, math.MinInt16, math.MaxInt16) {
|
||||
if minValue > maxValue {
|
||||
maxValue = minValue
|
||||
}
|
||||
}
|
||||
if gui.StepSliderInt("Maximum", &maxValue, math.MinInt16, math.MaxInt16) {
|
||||
if maxValue < minValue {
|
||||
minValue = maxValue
|
||||
}
|
||||
}
|
||||
if (minValue == math.MinInt16) && (maxValue == math.MaxInt16) {
|
||||
dialog.info.Limits = nil
|
||||
} else {
|
||||
if dialog.info.Limits == nil {
|
||||
dialog.info.Limits = &archive.GameVariableLimits{}
|
||||
}
|
||||
dialog.info.Limits.Minimum = int16(minValue)
|
||||
dialog.info.Limits.Maximum = int16(maxValue)
|
||||
}
|
||||
} else {
|
||||
imgui.LabelText("Minimum", fmt.Sprintf("%d", minValue))
|
||||
imgui.LabelText("Maximum", fmt.Sprintf("%d", maxValue))
|
||||
}
|
||||
|
||||
resetValue := int(dialog.info.ResetValueInt())
|
||||
if dialog.info.Hardcoded || !dialog.varOverride {
|
||||
imgui.LabelText("Reset Value", fmt.Sprintf("%d", resetValue))
|
||||
} else if gui.StepSliderInt("Reset Value", &resetValue, minValue, maxValue) {
|
||||
resetValue16 := int16(resetValue)
|
||||
dialog.info.InitValue = &resetValue16
|
||||
}
|
||||
}
|
||||
|
||||
imgui.PopItemWidth()
|
||||
}
|
||||
|
||||
type enumEntry struct {
|
||||
index int16
|
||||
value string
|
||||
}
|
||||
|
||||
func toEnumEntries(mapped map[int16]string) []enumEntry {
|
||||
minIndex := math.MaxInt16
|
||||
maxIndex := math.MinInt16
|
||||
for index := range mapped {
|
||||
if int(index) < minIndex {
|
||||
minIndex = int(index)
|
||||
}
|
||||
if int(index) > maxIndex {
|
||||
maxIndex = int(index)
|
||||
}
|
||||
}
|
||||
orderedEntries := make([]enumEntry, 0, len(mapped))
|
||||
for index, value := range mapped {
|
||||
orderedEntries = append(orderedEntries, enumEntry{
|
||||
index: index,
|
||||
value: value,
|
||||
})
|
||||
}
|
||||
sort.Slice(orderedEntries, func(a, b int) bool { return orderedEntries[a].index < orderedEntries[b].index })
|
||||
return orderedEntries
|
||||
}
|
||||
|
||||
func linearIndex(entries []enumEntry, index int16) int {
|
||||
for linear, entry := range entries {
|
||||
if entry.index == index {
|
||||
return linear
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package archives
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
)
|
||||
|
||||
type setArchiveDataCommand struct {
|
||||
model *viewModel
|
||||
|
||||
selectedLevel int
|
||||
|
||||
oldData map[resource.ID][]byte
|
||||
newData map[resource.ID][]byte
|
||||
}
|
||||
|
||||
func (command setArchiveDataCommand) Do(modder world.Modder) error {
|
||||
command.delResources(modder, command.oldData)
|
||||
return command.perform(modder, command.newData)
|
||||
}
|
||||
|
||||
func (command setArchiveDataCommand) Undo(modder world.Modder) error {
|
||||
command.delResources(modder, command.newData)
|
||||
return command.perform(modder, command.oldData)
|
||||
}
|
||||
|
||||
func (command setArchiveDataCommand) delResources(modder world.Modder, data map[resource.ID][]byte) {
|
||||
for id := range data {
|
||||
modder.DelResource(resource.LangAny, id)
|
||||
}
|
||||
}
|
||||
|
||||
// nolint: interfacer
|
||||
func (command setArchiveDataCommand) perform(modder world.Modder, data map[resource.ID][]byte) error {
|
||||
for id, blockData := range data {
|
||||
if len(blockData) > 0 {
|
||||
modder.SetResourceBlocks(resource.LangAny, id, [][]byte{blockData})
|
||||
} else {
|
||||
modder.DelResource(resource.LangAny, id)
|
||||
}
|
||||
}
|
||||
command.model.restoreFocus = true
|
||||
command.model.selectedLevel = command.selectedLevel
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,934 @@
|
||||
package archives
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/values"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlids"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlobj"
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
"github.com/inkyblackness/hacked/ss1/content/numbers"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||
"github.com/inkyblackness/hacked/ss1/edit"
|
||||
"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/citadel"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
const (
|
||||
hintUnknown = "???"
|
||||
)
|
||||
|
||||
var variableContextList = map[edit.VariableBaseContextIdentifier]struct {
|
||||
title string
|
||||
info string
|
||||
}{
|
||||
edit.VariableContextCitadel: {
|
||||
title: "Citadel Mission",
|
||||
info: "Variables are listed based on use on Citadel.\n" +
|
||||
"Useful for Citadel-based savegames and mods, such as 'New Game+' variants.",
|
||||
},
|
||||
edit.VariableContextEngine: {
|
||||
title: "Engine",
|
||||
info: "Variables are listed based on the hardcoded engine behaviour.\n" +
|
||||
"Useful for dedicated missions.",
|
||||
},
|
||||
}
|
||||
|
||||
// View provides edit controls for the archive.
|
||||
type View struct {
|
||||
registry cmd.Registry
|
||||
gameStateService *edit.GameStateService
|
||||
mod *world.Mod
|
||||
textCache *text.Cache
|
||||
cp text.Codepage
|
||||
|
||||
modalStateMachine gui.ModalStateMachine
|
||||
guiScale float32
|
||||
commander cmd.Commander
|
||||
|
||||
model viewModel
|
||||
}
|
||||
|
||||
// NewArchiveView returns a new instance.
|
||||
func NewArchiveView(registry cmd.Registry,
|
||||
gameStateService *edit.GameStateService, mod *world.Mod,
|
||||
textCache *text.Cache, cp text.Codepage,
|
||||
modalStateMachine gui.ModalStateMachine, guiScale float32, commander cmd.Commander) *View {
|
||||
view := &View{
|
||||
registry: registry,
|
||||
gameStateService: gameStateService,
|
||||
mod: mod,
|
||||
textCache: textCache,
|
||||
cp: cp,
|
||||
|
||||
modalStateMachine: modalStateMachine,
|
||||
guiScale: guiScale,
|
||||
commander: commander,
|
||||
|
||||
model: freshViewModel(),
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// WindowOpen returns the flag address, to be used with the main menu.
|
||||
func (view *View) WindowOpen() *bool {
|
||||
return &view.model.windowOpen
|
||||
}
|
||||
|
||||
// Render renders the view.
|
||||
func (view *View) Render() {
|
||||
if view.model.restoreFocus {
|
||||
imgui.SetNextWindowFocus()
|
||||
view.model.restoreFocus = false
|
||||
view.model.windowOpen = true
|
||||
}
|
||||
if view.model.windowOpen {
|
||||
imgui.SetNextWindowSizeV(imgui.Vec2{X: 650 * view.guiScale, Y: 400 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||
if imgui.BeginV("Archive", view.WindowOpen(), imgui.WindowFlagsNoCollapse) {
|
||||
view.renderContent()
|
||||
}
|
||||
imgui.End()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) renderContent() {
|
||||
imgui.BeginTabBar("archive-tab")
|
||||
|
||||
if imgui.BeginTabItem("Levels") {
|
||||
view.renderLevelsContent()
|
||||
imgui.EndTabItem()
|
||||
}
|
||||
if imgui.BeginTabItem("Game State") {
|
||||
view.renderGameStateContent()
|
||||
imgui.EndTabItem()
|
||||
}
|
||||
|
||||
imgui.EndTabBar()
|
||||
}
|
||||
|
||||
func (view *View) renderLevelsContent() {
|
||||
imgui.BeginChildV("Levels", imgui.Vec2{X: -100 * view.guiScale, Y: 0}, true, 0)
|
||||
for id := 0; id < archive.MaxLevels; id++ {
|
||||
inMod := view.hasLevelInMod(id)
|
||||
info := fmt.Sprintf("%d", id)
|
||||
if !inMod {
|
||||
if view.hasGameStateInMod() && (id == view.effectiveGameState().CurrentLevel()) {
|
||||
// current level should really be in the archive.
|
||||
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1.0, Y: 0.0, Z: 0.0, W: 1.0})
|
||||
} else {
|
||||
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1.0, Y: 1.0, Z: 1.0, W: 0.8})
|
||||
}
|
||||
info += " (not in mod, read-only)"
|
||||
}
|
||||
if imgui.SelectableV(info, id == view.model.selectedLevel, 0, imgui.Vec2{}) {
|
||||
view.model.selectedLevel = id
|
||||
}
|
||||
if !inMod {
|
||||
imgui.PopStyleColor()
|
||||
}
|
||||
}
|
||||
imgui.EndChild()
|
||||
imgui.SameLine()
|
||||
imgui.BeginGroup()
|
||||
if imgui.ButtonV("Clear", imgui.Vec2{X: -1, Y: 0}) {
|
||||
view.requestClearLevel(view.model.selectedLevel)
|
||||
}
|
||||
if imgui.ButtonV("Remove", imgui.Vec2{X: -1, Y: 0}) {
|
||||
view.requestRemoveLevel(view.model.selectedLevel)
|
||||
}
|
||||
imgui.EndGroup()
|
||||
}
|
||||
|
||||
func (view *View) renderGameStateContent() {
|
||||
data := view.gameStateData()
|
||||
modStateIsDefaulting := false
|
||||
|
||||
imgui.PushItemWidth(-350 * view.guiScale)
|
||||
if data.isPresent() {
|
||||
modState := data.toInstance()
|
||||
if !modState.IsSavegame() {
|
||||
modStateIsDefaulting = modState.IsDefaulting()
|
||||
if modStateIsDefaulting {
|
||||
if imgui.Button("Override") {
|
||||
view.requestSetGameState(citadel.DefaultGameState().Raw())
|
||||
}
|
||||
if imgui.IsItemHovered() {
|
||||
imgui.SetTooltip("Values of new game archives are only considered by special engines.\n" +
|
||||
"Overriding them will only work in those.")
|
||||
}
|
||||
} else if imgui.Button("Remove") {
|
||||
view.requestSetGameState(archive.ZeroGameStateData())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readOnly := false
|
||||
var editState *archive.GameState
|
||||
if !data.isPresent() || modStateIsDefaulting {
|
||||
editState = citadel.DefaultGameState()
|
||||
readOnly = true
|
||||
} else {
|
||||
editState = data.imported().toInstance()
|
||||
}
|
||||
|
||||
onChange := func() {
|
||||
view.requestSetGameState(editState.Raw())
|
||||
}
|
||||
if imgui.TreeNodeV("Game State: General", imgui.TreeNodeFlagsDefaultOpen|imgui.TreeNodeFlagsFramed) {
|
||||
imgui.LabelText("Hacker Name", "\""+editState.HackerName(view.cp)+"\"")
|
||||
view.createPropertyControls(readOnly, editState.Instance, func(key string, modifier func(uint32) uint32) {
|
||||
view.setInterpreterValueKeyed(editState.Instance, key, modifier)
|
||||
onChange()
|
||||
})
|
||||
imgui.TreePop()
|
||||
}
|
||||
if imgui.TreeNodeV("Inventory", imgui.TreeNodeFlagsFramed) {
|
||||
view.createInventoryControls(readOnly, editState, onChange)
|
||||
imgui.TreePop()
|
||||
}
|
||||
if imgui.TreeNodeV("Hard-/Software", imgui.TreeNodeFlagsFramed) {
|
||||
view.createWareControls(readOnly, editState, onChange)
|
||||
imgui.TreePop()
|
||||
}
|
||||
if imgui.TreeNodeV("Game State: Variables", imgui.TreeNodeFlagsFramed) {
|
||||
view.createVariableControls(readOnly, editState, onChange)
|
||||
imgui.TreePop()
|
||||
}
|
||||
if imgui.TreeNodeV("Game State: Messages", imgui.TreeNodeFlagsFramed) {
|
||||
view.createMessageControls(readOnly, editState, onChange)
|
||||
imgui.TreePop()
|
||||
}
|
||||
|
||||
imgui.PopItemWidth()
|
||||
}
|
||||
|
||||
type gameStateData struct{ raw []byte }
|
||||
|
||||
func (data gameStateData) isPresent() bool {
|
||||
return len(data.raw) > 0
|
||||
}
|
||||
|
||||
func (data gameStateData) toInstance() *archive.GameState {
|
||||
return archive.NewGameState(data.raw)
|
||||
}
|
||||
|
||||
func (data gameStateData) snapshot() gameStateData {
|
||||
copied := make([]byte, len(data.raw))
|
||||
copy(copied, data.raw)
|
||||
return gameStateData{raw: copied}
|
||||
}
|
||||
|
||||
func (data gameStateData) imported() gameStateData {
|
||||
// Avoid truncating data when working with savegames.
|
||||
if len(data.raw) >= archive.GameStateSize {
|
||||
return data.snapshot()
|
||||
}
|
||||
copied := archive.ZeroGameStateData()
|
||||
copy(copied, data.raw)
|
||||
return gameStateData{raw: copied}
|
||||
}
|
||||
|
||||
func (view *View) gameStateData() gameStateData {
|
||||
raw := view.mod.ModifiedBlock(resource.LangAny, ids.GameState, 0)
|
||||
return gameStateData{raw: raw}
|
||||
}
|
||||
|
||||
func (view *View) effectiveGameState() *archive.GameState {
|
||||
state := view.gameStateData().toInstance()
|
||||
if state.IsDefaulting() {
|
||||
return citadel.DefaultGameState()
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func (view *View) hasGameStateInMod() bool {
|
||||
return len(view.mod.ModifiedBlocks(resource.LangAny, ids.GameState)) > 0
|
||||
}
|
||||
|
||||
func (view *View) hasLevelInMod(id int) bool {
|
||||
return len(view.mod.ModifiedBlocks(resource.LangAny, ids.LevelResourcesStart.Plus(lvlids.PerLevel*id+lvlids.FirstUsed))) > 0
|
||||
}
|
||||
|
||||
func (view *View) requestClearLevel(id int) {
|
||||
if (id >= 0) && (id < archive.MaxLevels) {
|
||||
command := setArchiveDataCommand{
|
||||
model: &view.model,
|
||||
selectedLevel: id,
|
||||
newData: make(map[resource.ID][]byte),
|
||||
oldData: make(map[resource.ID][]byte),
|
||||
}
|
||||
|
||||
if !view.hasGameStateInMod() {
|
||||
command.newData[ids.ArchiveName] = text.DefaultCodepage().Encode("Starting Game | by InkyBlackness HackEd")
|
||||
command.newData[ids.GameState] = archive.ZeroGameStateData()
|
||||
}
|
||||
|
||||
param := level.EmptyLevelParameters{
|
||||
Cyberspace: world.IsConsideredCyberspaceByDefault(id),
|
||||
}
|
||||
effectiveGameState := view.effectiveGameState()
|
||||
if id == effectiveGameState.CurrentLevel() {
|
||||
posX, posY := effectiveGameState.HackerMapPosition()
|
||||
param.MapModifier = func(x, y int, entry *level.TileMapEntry) {
|
||||
if (x == int(posX.Tile())) && (y == int(posY.Tile())) {
|
||||
entry.Type = level.TileTypeOpen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
levelData := level.EmptyLevelData(param)
|
||||
levelIDBegin := ids.LevelResourcesStart.Plus(lvlids.PerLevel * id)
|
||||
for offset, newData := range &levelData {
|
||||
if (offset < lvlids.FirstUsed) || (offset >= lvlids.FirstUnused) {
|
||||
continue
|
||||
}
|
||||
resourceID := levelIDBegin.Plus(offset)
|
||||
oldData := view.mod.ModifiedBlock(resource.LangAny, resourceID, 0)
|
||||
if len(oldData) > 0 {
|
||||
command.oldData[resourceID] = oldData
|
||||
}
|
||||
if len(newData) > 0 {
|
||||
command.newData[resourceID] = newData
|
||||
}
|
||||
}
|
||||
|
||||
view.commander.Queue(command)
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) requestRemoveLevel(id int) {
|
||||
if (id >= 0) && (id < archive.MaxLevels) && view.hasLevelInMod(id) {
|
||||
command := setArchiveDataCommand{
|
||||
model: &view.model,
|
||||
selectedLevel: id,
|
||||
newData: make(map[resource.ID][]byte),
|
||||
oldData: make(map[resource.ID][]byte),
|
||||
}
|
||||
|
||||
levelIDBegin := ids.LevelResourcesStart.Plus(lvlids.PerLevel * id)
|
||||
for offset := lvlids.FirstUsed; offset < lvlids.PerLevel; offset++ {
|
||||
resourceID := levelIDBegin.Plus(offset)
|
||||
oldData := view.mod.ModifiedBlock(resource.LangAny, resourceID, 0)
|
||||
if len(oldData) > 0 {
|
||||
command.oldData[resourceID] = oldData
|
||||
}
|
||||
}
|
||||
|
||||
view.commander.Queue(command)
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) requestSetGameState(newData []byte) {
|
||||
command := setArchiveDataCommand{
|
||||
model: &view.model,
|
||||
selectedLevel: view.model.selectedLevel,
|
||||
newData: make(map[resource.ID][]byte),
|
||||
oldData: make(map[resource.ID][]byte),
|
||||
}
|
||||
|
||||
command.oldData[ids.GameState] = view.gameStateData().snapshot().raw
|
||||
command.newData[ids.GameState] = newData
|
||||
|
||||
view.commander.Queue(command)
|
||||
}
|
||||
|
||||
func (view *View) createPropertyControls(readOnly bool, rootInterpreter *interpreters.Instance,
|
||||
updater func(string, func(uint32) uint32)) {
|
||||
objTypeRenderer := values.ObjectTypeControlRenderer{
|
||||
Meta: view.mod.ObjectProperties(),
|
||||
TextCache: view.textCache,
|
||||
}
|
||||
|
||||
var processInterpreter func(string, *interpreters.Instance)
|
||||
processInterpreter = func(path string, interpreter *interpreters.Instance) {
|
||||
for _, key := range interpreter.Keys() {
|
||||
fullKey := path + key
|
||||
unifier := values.NewUnifier()
|
||||
unifier.Add(int32(interpreter.Get(key)))
|
||||
simplifier := values.StandardSimplifier(readOnly, fullKey, unifier,
|
||||
func(modifier func(uint32) uint32) {
|
||||
updater(fullKey, modifier)
|
||||
}, objTypeRenderer)
|
||||
|
||||
interpreter.Describe(key, simplifier)
|
||||
}
|
||||
|
||||
for _, key := range interpreter.ActiveRefinements() {
|
||||
fullKey := path + key
|
||||
if len(fullKey) > 0 {
|
||||
imgui.Separator()
|
||||
imgui.Text(fullKey + ":")
|
||||
}
|
||||
processInterpreter(fullKey+".", interpreter.Refined(key))
|
||||
}
|
||||
}
|
||||
processInterpreter("", rootInterpreter)
|
||||
}
|
||||
|
||||
func (view *View) setInterpreterValueKeyed(instance *interpreters.Instance, key string, modifier func(uint32) uint32) {
|
||||
resolvedInterpreter := instance
|
||||
keys := strings.Split(key, ".")
|
||||
keyCount := len(keys)
|
||||
if len(keys) > 1 {
|
||||
for _, subKey := range keys[:keyCount-1] {
|
||||
resolvedInterpreter = resolvedInterpreter.Refined(subKey)
|
||||
}
|
||||
}
|
||||
resolvedInterpreter.Set(keys[keyCount-1], modifier(resolvedInterpreter.Get(keys[keyCount-1])))
|
||||
}
|
||||
|
||||
func (view *View) createInventoryControls(readOnly bool, gameState *archive.GameState, onChange func()) {
|
||||
if imgui.TreeNodeV("Weapons", imgui.TreeNodeFlagsFramed) {
|
||||
for i := 0; i < archive.InventoryWeaponSlots; i++ {
|
||||
if i > 0 {
|
||||
imgui.Separator()
|
||||
}
|
||||
imgui.PushID(fmt.Sprintf("weapon%d", i))
|
||||
view.createInventoryWeaponSlotControls(readOnly, gameState.InventoryWeaponSlot(i), onChange)
|
||||
imgui.PopID()
|
||||
}
|
||||
imgui.TreePop()
|
||||
}
|
||||
if imgui.TreeNodeV("Ammo", imgui.TreeNodeFlagsFramed) {
|
||||
for i := 0; i < archive.AmmoTypeCount; i++ {
|
||||
imgui.Separator()
|
||||
imgui.PushID(fmt.Sprintf("ammo%d", i))
|
||||
view.createInventoryAmmoControls(readOnly, gameState.InventoryAmmo(i), onChange)
|
||||
imgui.PopID()
|
||||
}
|
||||
imgui.TreePop()
|
||||
}
|
||||
if imgui.TreeNodeV("Grenades", imgui.TreeNodeFlagsFramed) {
|
||||
for i := 0; i < archive.GrenadeTypeCount; i++ {
|
||||
imgui.Separator()
|
||||
imgui.PushID(fmt.Sprintf("grenade%d", i))
|
||||
view.createInventoryGrenadeControls(readOnly, gameState.InventoryGrenade(i), onChange)
|
||||
imgui.PopID()
|
||||
}
|
||||
imgui.TreePop()
|
||||
}
|
||||
if imgui.TreeNodeV("Patches", imgui.TreeNodeFlagsFramed) {
|
||||
for i := 0; i < archive.GrenadeTypeCount; i++ {
|
||||
imgui.PushID(fmt.Sprintf("patch%d", i))
|
||||
view.createPatchStateControls(readOnly, gameState.PatchState(i), onChange)
|
||||
imgui.PopID()
|
||||
}
|
||||
imgui.TreePop()
|
||||
}
|
||||
if imgui.TreeNodeV("General", imgui.TreeNodeFlagsFramed) {
|
||||
for i := 0; i < archive.GeneralInventorySlotCount; i++ {
|
||||
imgui.PushID(fmt.Sprintf("general%d", i))
|
||||
view.createGeneralInventoryControls(readOnly, gameState.GeneralInventorySlot(i), onChange)
|
||||
imgui.PopID()
|
||||
}
|
||||
imgui.TreePop()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) createWareControls(readOnly bool, gameState *archive.GameState, onChange func()) {
|
||||
if imgui.TreeNodeV("Hardware", imgui.TreeNodeFlagsFramed) {
|
||||
for i := 0; i < archive.HardwareTypeCount; i++ {
|
||||
imgui.PushID(fmt.Sprintf("hw%d", i))
|
||||
view.createHardwareControls(readOnly, gameState.HardwareState(i), onChange)
|
||||
imgui.PopID()
|
||||
}
|
||||
imgui.TreePop()
|
||||
}
|
||||
|
||||
if imgui.TreeNodeV("Software", imgui.TreeNodeFlagsFramed) {
|
||||
for i := 0; i < archive.SoftwareOffenseTypeCount; i++ {
|
||||
imgui.PushID(fmt.Sprintf("swoffense%d", i))
|
||||
view.createVersionedSoftwareControls(readOnly, gameState.VersionedOffenseSoftwareState(i), 0, onChange)
|
||||
imgui.PopID()
|
||||
}
|
||||
for i := 0; i < archive.SoftwareDefenseTypeCount; i++ {
|
||||
imgui.PushID(fmt.Sprintf("swdefense%d", i))
|
||||
view.createVersionedSoftwareControls(readOnly, gameState.VersionedDefenseSoftwareState(i), 1, onChange)
|
||||
imgui.PopID()
|
||||
}
|
||||
for i := 0; i < archive.SoftwareOneshotTypeCount; i++ {
|
||||
imgui.PushID(fmt.Sprintf("swoneshot%d", i))
|
||||
view.createCountedSoftwareControls(readOnly, gameState.OneshotSoftwareState(i), 2, onChange)
|
||||
imgui.PopID()
|
||||
}
|
||||
imgui.TreePop()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) createHardwareControls(readOnly bool, state archive.HardwareState, onChange func()) {
|
||||
values.RenderUnifiedSliderInt(readOnly,
|
||||
fmt.Sprintf("Version (%s)", view.indexedName(object.ClassHardware, state.Index)),
|
||||
values.UnifierFor(state.Version()),
|
||||
func(u values.Unifier) int {
|
||||
return u.Unified().(int)
|
||||
}, func(value int) string {
|
||||
return "%d"
|
||||
}, 0, 4,
|
||||
func(newValue int) {
|
||||
state.SetVersion(newValue)
|
||||
onChange()
|
||||
})
|
||||
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "Is Active", values.UnifierFor(state.IsActive()), func(newActive bool) {
|
||||
state.SetActive(newActive)
|
||||
onChange()
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) createVersionedSoftwareControls(readOnly bool, sw archive.VersionedSoftwareState, subclass int, onChange func()) {
|
||||
values.RenderUnifiedSliderInt(readOnly,
|
||||
fmt.Sprintf("Version (%s)", view.tripleName(object.TripleFrom(int(object.ClassSoftware), subclass, sw.Index))),
|
||||
values.UnifierFor(sw.Version()),
|
||||
func(u values.Unifier) int {
|
||||
return u.Unified().(int)
|
||||
}, func(value int) string {
|
||||
return "%d"
|
||||
}, 0, 0xFF,
|
||||
func(newValue int) {
|
||||
sw.SetVersion(newValue)
|
||||
onChange()
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) createCountedSoftwareControls(readOnly bool, sw archive.CountedSoftwareState, subclass int, onChange func()) {
|
||||
values.RenderUnifiedSliderInt(readOnly,
|
||||
fmt.Sprintf("Count (%s)", view.tripleName(object.TripleFrom(int(object.ClassSoftware), subclass, sw.Index))),
|
||||
values.UnifierFor(sw.Count()),
|
||||
func(u values.Unifier) int {
|
||||
return u.Unified().(int)
|
||||
}, func(value int) string {
|
||||
return "%d"
|
||||
}, 0, 0xFF,
|
||||
func(newValue int) {
|
||||
sw.SetCount(newValue)
|
||||
onChange()
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) createGeneralInventoryControls(readOnly bool, slot archive.GeneralInventorySlot, onChange func()) {
|
||||
values.RenderUnifiedSliderInt(readOnly,
|
||||
fmt.Sprintf("ObjectIndex (Slot %d)", slot.Index+1),
|
||||
values.UnifierFor(slot.ObjectID()),
|
||||
func(u values.Unifier) int {
|
||||
return int(u.Unified().(level.ObjectID))
|
||||
}, func(value int) string {
|
||||
return "%d"
|
||||
}, 0, 871, // TODO: get ID limit of current level
|
||||
func(newValue int) {
|
||||
slot.SetObjectID(level.ObjectID(newValue))
|
||||
onChange()
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) createPatchStateControls(readOnly bool, patch archive.PatchState, onChange func()) {
|
||||
values.RenderUnifiedSliderInt(readOnly,
|
||||
fmt.Sprintf("Patch Count (%s)", view.indexedName(object.ClassDrug, patch.Index)),
|
||||
values.UnifierFor(patch.Count()),
|
||||
func(u values.Unifier) int {
|
||||
return u.Unified().(int)
|
||||
}, func(value int) string {
|
||||
return "%d"
|
||||
}, 0, 0xFF,
|
||||
func(newValue int) {
|
||||
patch.SetCount(newValue)
|
||||
onChange()
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) createInventoryGrenadeControls(readOnly bool, grenade archive.InventoryGrenade, onChange func()) {
|
||||
values.RenderUnifiedSliderInt(readOnly,
|
||||
fmt.Sprintf("Grenade Count (%s)", view.indexedName(object.ClassGrenade, grenade.Index)),
|
||||
values.UnifierFor(grenade.Count()),
|
||||
func(u values.Unifier) int {
|
||||
return u.Unified().(int)
|
||||
}, func(value int) string {
|
||||
return "%d"
|
||||
}, 0, 0xFF,
|
||||
func(newValue int) {
|
||||
grenade.SetCount(newValue)
|
||||
onChange()
|
||||
})
|
||||
|
||||
values.RenderUnifiedSliderInt(readOnly, "Timer", values.UnifierFor(grenade.TimerSetting()),
|
||||
func(u values.Unifier) int {
|
||||
return int(u.Unified().(archive.GrenadeTimerSetting))
|
||||
}, func(value int) string {
|
||||
setting := archive.GrenadeTimerSetting(value)
|
||||
return fmt.Sprintf("%.01fs -- raw: %%d", setting.InSeconds())
|
||||
}, 0, archive.GrenadeTimerMaximum,
|
||||
func(newValue int) {
|
||||
grenade.SetTimerSetting(archive.GrenadeTimerSetting(newValue))
|
||||
onChange()
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) createInventoryAmmoControls(readOnly bool, ammo archive.InventoryAmmo, onChange func()) {
|
||||
values.RenderUnifiedSliderInt(readOnly,
|
||||
fmt.Sprintf("Clip Count (%s)", view.indexedName(object.ClassAmmo, ammo.Index)),
|
||||
values.UnifierFor(ammo.FullClipCount()),
|
||||
func(u values.Unifier) int {
|
||||
return u.Unified().(int)
|
||||
}, func(value int) string {
|
||||
return "%d"
|
||||
}, 0, 0xFF,
|
||||
func(newValue int) {
|
||||
ammo.SetFullClipCount(newValue)
|
||||
onChange()
|
||||
})
|
||||
values.RenderUnifiedSliderInt(readOnly,
|
||||
"Extra rounds",
|
||||
values.UnifierFor(ammo.ExtraRoundsCount()),
|
||||
func(u values.Unifier) int {
|
||||
return u.Unified().(int)
|
||||
}, func(value int) string {
|
||||
return "%d"
|
||||
}, 0, 0xFF,
|
||||
func(newValue int) {
|
||||
ammo.SetExtraRoundsCount(newValue)
|
||||
onChange()
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) createInventoryWeaponSlotControls(readOnly bool, slot archive.InventoryWeaponSlot, onChange func()) {
|
||||
isInUse := slot.IsInUse()
|
||||
inUseName := fmt.Sprintf("Weapon %d In Use", slot.Index+1)
|
||||
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, inUseName, values.UnifierFor(isInUse), func(newUse bool) {
|
||||
if newUse {
|
||||
slot.SetInUse(0, 0)
|
||||
} else {
|
||||
slot.SetFree()
|
||||
}
|
||||
onChange()
|
||||
})
|
||||
if !isInUse {
|
||||
return
|
||||
}
|
||||
|
||||
currentWeaponTriple := slot.Triple()
|
||||
if imgui.BeginCombo("Type", view.tripleName(currentWeaponTriple)) {
|
||||
allTypes := view.mod.ObjectProperties().TriplesInClass(currentWeaponTriple.Class)
|
||||
for _, triple := range allTypes {
|
||||
if imgui.SelectableV(view.tripleName(triple), triple == currentWeaponTriple, 0, imgui.Vec2{}) {
|
||||
slot.SetInUse(triple.Subclass, triple.Type)
|
||||
onChange()
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
isEnergyWeapon := currentWeaponTriple.Subclass >= 4
|
||||
if isEnergyWeapon {
|
||||
temperature, chargeAndOverload := slot.WeaponState()
|
||||
values.RenderUnifiedSliderInt(readOnly, "Temperature", values.UnifierFor(int(temperature)),
|
||||
func(u values.Unifier) int {
|
||||
return u.Unified().(int)
|
||||
}, func(value int) string {
|
||||
return fmt.Sprintf("%.02f%%%% -- raw: %%d", (float32(value)/float32(lvlobj.EnergyWeaponMaxTemperature))*100)
|
||||
}, 0x00, lvlobj.EnergyWeaponMaxTemperature,
|
||||
func(newValue int) {
|
||||
slot.SetWeaponState(byte(newValue), chargeAndOverload)
|
||||
onChange()
|
||||
})
|
||||
|
||||
overloadState := chargeAndOverload & 0x80
|
||||
charge := chargeAndOverload & 0x7F
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "Overload", values.UnifierFor(overloadState != 0), func(newOverload bool) {
|
||||
if newOverload {
|
||||
slot.SetWeaponState(temperature, 0x80|charge)
|
||||
} else {
|
||||
slot.SetWeaponState(temperature, charge)
|
||||
}
|
||||
onChange()
|
||||
})
|
||||
values.RenderUnifiedSliderInt(readOnly, "Charge", values.UnifierFor(int(charge)),
|
||||
func(u values.Unifier) int {
|
||||
return u.Unified().(int)
|
||||
}, func(value int) string {
|
||||
return fmt.Sprintf("%.02f%%%% -- raw: %%d", (float32(value)/float32(lvlobj.EnergyWeaponMaxEnergy))*100)
|
||||
}, 0x00, 0x7F,
|
||||
func(newValue int) {
|
||||
slot.SetWeaponState(temperature, overloadState|byte(newValue))
|
||||
onChange()
|
||||
})
|
||||
} else {
|
||||
rounds, ammoType := slot.WeaponState()
|
||||
values.RenderUnifiedSliderInt(readOnly, "Rounds", values.UnifierFor(int(rounds)),
|
||||
func(u values.Unifier) int {
|
||||
return u.Unified().(int)
|
||||
}, func(value int) string {
|
||||
return fmt.Sprintf("%d", value)
|
||||
}, 0x00, 0xFF,
|
||||
func(newValue int) {
|
||||
slot.SetWeaponState(byte(newValue), ammoType)
|
||||
onChange()
|
||||
})
|
||||
values.RenderUnifiedCombo(readOnly, "Ammo Type", values.UnifierFor(int(ammoType)),
|
||||
func(u values.Unifier) int {
|
||||
return u.Unified().(int)
|
||||
},
|
||||
func(value int) string {
|
||||
switch value {
|
||||
case 0:
|
||||
return "Standard"
|
||||
case 1:
|
||||
return "Special"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown %d", ammoType)
|
||||
}
|
||||
}, 2,
|
||||
func(newValue int) {
|
||||
slot.SetWeaponState(rounds, byte(newValue))
|
||||
onChange()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) createVariableControls(readOnly bool, gameState *archive.GameState, onChange func()) {
|
||||
currentContextIdentifier := view.gameStateService.VariableBaseContext()
|
||||
if imgui.BeginCombo("Base Context", variableContextList[currentContextIdentifier].title) {
|
||||
contextIdentifiers := edit.VariableContextIdentifiers()
|
||||
for _, identifier := range contextIdentifiers {
|
||||
contextInfo := variableContextList[identifier]
|
||||
if imgui.SelectableV(contextInfo.title, identifier == currentContextIdentifier, 0, imgui.Vec2{}) {
|
||||
view.gameStateService.SetVariableBaseContext(identifier)
|
||||
}
|
||||
if imgui.IsItemHovered() {
|
||||
imgui.SetTooltip(contextInfo.info)
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
if imgui.IsItemHovered() {
|
||||
imgui.SetTooltip("The base context defines to what variables default to\n" +
|
||||
"in case they are not overridden by the project.")
|
||||
}
|
||||
|
||||
if imgui.Button("Remove All Overrides") {
|
||||
_ = view.gameStateService.DefaultAllVariables()
|
||||
}
|
||||
if imgui.IsItemHovered() {
|
||||
imgui.SetTooltip("Removes all overrides and their parameters.")
|
||||
}
|
||||
if !readOnly {
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Reset All") {
|
||||
for i := 0; i < archive.BooleanVarCount; i++ {
|
||||
gameState.SetBooleanVar(i, view.gameStateService.BooleanVariable(i).ResetValueBool())
|
||||
}
|
||||
for i := 0; i < archive.IntegerVarCount; i++ {
|
||||
gameState.SetIntegerVar(i, view.gameStateService.IntegerVariable(i).ResetValueInt())
|
||||
}
|
||||
onChange()
|
||||
}
|
||||
if imgui.IsItemHovered() {
|
||||
imgui.SetTooltip("Resets all variables to their configured initial value.\n" +
|
||||
"To reset a single variable, hover over the specific variable and use the context menu.")
|
||||
}
|
||||
}
|
||||
|
||||
isSavegame := gameState.IsSavegame()
|
||||
if imgui.TreeNodeV("Boolean Variables", imgui.TreeNodeFlagsFramed) {
|
||||
intConverter := func(u values.Unifier) int {
|
||||
bValue := u.Unified().(bool)
|
||||
if bValue {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
defaultNames := map[int]string{0: "False", 1: "True"}
|
||||
|
||||
for i := 0; i < archive.BooleanVarCount; i++ {
|
||||
varIndex := i
|
||||
info := view.gameStateService.BooleanVariable(varIndex)
|
||||
|
||||
varReadOnly := readOnly || (info.Hardcoded && !isSavegame)
|
||||
varLabel := fmt.Sprintf("Var%03d", varIndex)
|
||||
varName := fmt.Sprintf("Var%03d: %s", varIndex, info.Name)
|
||||
varUnifier := values.UnifierFor(gameState.BooleanVar(varIndex))
|
||||
|
||||
values.RenderUnifiedCombo(varReadOnly, varName, varUnifier, intConverter,
|
||||
func(value int) string {
|
||||
name, found := info.ValueNames[int16(value)]
|
||||
if found {
|
||||
return name
|
||||
}
|
||||
return defaultNames[value]
|
||||
},
|
||||
2,
|
||||
func(newValue int) {
|
||||
gameState.SetBooleanVar(varIndex, newValue != 0)
|
||||
onChange()
|
||||
})
|
||||
|
||||
if imgui.BeginPopupContextItemV(varLabel+"-Popup", 1) {
|
||||
if imgui.Selectable("Edit info...") {
|
||||
view.modalStateMachine.SetState(&editBooleanVariableDialog{
|
||||
machine: view.modalStateMachine,
|
||||
view: view,
|
||||
service: view.gameStateService,
|
||||
index: varIndex,
|
||||
})
|
||||
}
|
||||
if !varReadOnly {
|
||||
imgui.Separator()
|
||||
if imgui.Selectable("Reset") {
|
||||
gameState.SetBooleanVar(varIndex, info.ResetValueBool())
|
||||
onChange()
|
||||
}
|
||||
}
|
||||
imgui.EndPopup()
|
||||
} else if imgui.IsItemHovered() && (len(info.Description) > 0) {
|
||||
imgui.SetTooltip(info.Description)
|
||||
}
|
||||
}
|
||||
imgui.TreePop()
|
||||
}
|
||||
if imgui.TreeNodeV("Integer Variables", imgui.TreeNodeFlagsFramed) {
|
||||
intConverter := func(u values.Unifier) int {
|
||||
return int(u.Unified().(int16))
|
||||
}
|
||||
for i := 0; i < archive.IntegerVarCount; i++ {
|
||||
varIndex := i
|
||||
info := view.gameStateService.IntegerVariable(varIndex)
|
||||
|
||||
varReadOnly := readOnly || (info.Hardcoded && !isSavegame)
|
||||
varLabel := fmt.Sprintf("Var%02d", varIndex)
|
||||
varName := fmt.Sprintf("Var%02d: %s", varIndex, info.Name)
|
||||
varUnifier := values.UnifierFor(gameState.IntegerVar(varIndex))
|
||||
changeHandler := func(newValue int) {
|
||||
gameState.SetIntegerVar(varIndex, int16(newValue))
|
||||
onChange()
|
||||
}
|
||||
|
||||
switch {
|
||||
case archive.IsRandomIntegerVariable(varIndex):
|
||||
values.RenderUnifiedSliderInt(readOnly, varName, varUnifier,
|
||||
func(u values.Unifier) int { return int(numbers.FromBinaryCodedDecimal(uint16(intConverter(u)))) },
|
||||
func(value int) string { return "%03d" },
|
||||
0, 999,
|
||||
func(newValue int) {
|
||||
changeHandler(int(numbers.ToBinaryCodedDecimal(uint16(newValue))))
|
||||
})
|
||||
case len(info.ValueNames) > 0:
|
||||
var max int16
|
||||
for key := range info.ValueNames {
|
||||
if key > max {
|
||||
max = key
|
||||
}
|
||||
}
|
||||
values.RenderUnifiedCombo(varReadOnly, varName, varUnifier, intConverter,
|
||||
func(value int) string {
|
||||
name, found := info.ValueNames[int16(value)]
|
||||
if found {
|
||||
return name
|
||||
}
|
||||
return fmt.Sprintf("Unknown %d", value)
|
||||
},
|
||||
int(max+1),
|
||||
changeHandler)
|
||||
default:
|
||||
min := math.MinInt16
|
||||
max := math.MaxInt16
|
||||
if info.Limits != nil {
|
||||
min = int(info.Limits.Minimum)
|
||||
max = int(info.Limits.Maximum)
|
||||
}
|
||||
|
||||
values.RenderUnifiedSliderInt(varReadOnly, varName, varUnifier, intConverter,
|
||||
func(value int) string {
|
||||
return "%d"
|
||||
},
|
||||
min, max,
|
||||
changeHandler)
|
||||
}
|
||||
if imgui.BeginPopupContextItemV(varLabel+"-Popup", 1) {
|
||||
if imgui.Selectable("Edit info...") {
|
||||
view.modalStateMachine.SetState(&editIntegerVariableDialog{
|
||||
machine: view.modalStateMachine,
|
||||
view: view,
|
||||
service: view.gameStateService,
|
||||
index: varIndex,
|
||||
})
|
||||
}
|
||||
if !varReadOnly {
|
||||
imgui.Separator()
|
||||
if imgui.Selectable("Reset") {
|
||||
gameState.SetIntegerVar(varIndex, info.ResetValueInt())
|
||||
onChange()
|
||||
}
|
||||
}
|
||||
imgui.EndPopup()
|
||||
} else if imgui.IsItemHovered() && (len(info.Description) > 0) {
|
||||
imgui.SetTooltip(info.Description)
|
||||
}
|
||||
}
|
||||
imgui.TreePop()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) createMessageControls(readOnly bool, gameState *archive.GameState, onChange func()) {
|
||||
view.createMessageControlsFor(readOnly, "EMail",
|
||||
&view.model.emailIndex, archive.EMailCount, gameState.EMailState, onChange)
|
||||
view.createMessageControlsFor(readOnly, "Log",
|
||||
&view.model.logIndex, archive.LogCount, gameState.LogState, onChange)
|
||||
view.createMessageControlsFor(readOnly, "Fragment",
|
||||
&view.model.fragmentIndex, archive.FragmentCount, gameState.FragmentState, onChange)
|
||||
}
|
||||
|
||||
func (view *View) createMessageControlsFor(readOnly bool, name string, index *int, limit int,
|
||||
retriever func(int) archive.MessageState, onChange func()) {
|
||||
imgui.PushID(name)
|
||||
gui.StepSliderInt(name+" Index", index, 0, limit-1)
|
||||
messageState := retriever(*index)
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "Received", values.UnifierFor(messageState.Received()),
|
||||
func(newState bool) {
|
||||
messageState.SetReceived(newState)
|
||||
onChange()
|
||||
})
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "Read", values.UnifierFor(messageState.Read()),
|
||||
func(newState bool) {
|
||||
messageState.SetRead(newState)
|
||||
onChange()
|
||||
})
|
||||
imgui.PopID()
|
||||
}
|
||||
|
||||
func (view *View) tripleName(triple object.Triple) string {
|
||||
suffix := hintUnknown
|
||||
linearIndex := view.mod.ObjectProperties().TripleIndex(triple)
|
||||
if linearIndex >= 0 {
|
||||
key := resource.KeyOf(ids.ObjectLongNames, resource.LangDefault, linearIndex)
|
||||
objName, err := view.textCache.Text(key)
|
||||
if err == nil {
|
||||
suffix = singleLineString(objName)
|
||||
}
|
||||
}
|
||||
return triple.String() + ": " + suffix
|
||||
}
|
||||
|
||||
func (view *View) indexedName(class object.Class, index int) string {
|
||||
startIndex := view.mod.ObjectProperties().TripleIndex(object.TripleFrom(int(class), 0, 0))
|
||||
if startIndex >= 0 {
|
||||
key := resource.KeyOf(ids.ObjectLongNames, resource.LangDefault, startIndex+index)
|
||||
objName, err := view.textCache.Text(key)
|
||||
if err == nil {
|
||||
return singleLineString(objName)
|
||||
}
|
||||
}
|
||||
return hintUnknown
|
||||
}
|
||||
|
||||
func singleLineString(str string) string {
|
||||
return strings.ReplaceAll(strings.ReplaceAll(str, "\r", ""), "\n", "")
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package archives
|
||||
|
||||
type viewModel struct {
|
||||
windowOpen bool
|
||||
restoreFocus bool
|
||||
|
||||
selectedLevel int
|
||||
|
||||
emailIndex int
|
||||
logIndex int
|
||||
fragmentIndex int
|
||||
}
|
||||
|
||||
func freshViewModel() viewModel {
|
||||
return viewModel{}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package bitmaps
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
)
|
||||
|
||||
type setBitmapCommand struct {
|
||||
model *viewModel
|
||||
|
||||
displayKey resource.Key
|
||||
|
||||
resourceKey resource.Key
|
||||
oldData []byte
|
||||
newData []byte
|
||||
}
|
||||
|
||||
func (cmd setBitmapCommand) Do(modder world.Modder) error {
|
||||
return cmd.perform(modder, cmd.newData)
|
||||
}
|
||||
|
||||
func (cmd setBitmapCommand) Undo(modder world.Modder) error {
|
||||
return cmd.perform(modder, cmd.oldData)
|
||||
}
|
||||
|
||||
func (cmd setBitmapCommand) perform(modder world.Modder, data []byte) error {
|
||||
modder.SetResourceBlock(cmd.resourceKey.Lang, cmd.resourceKey.ID, cmd.resourceKey.Index, data)
|
||||
|
||||
cmd.model.restoreFocus = true
|
||||
cmd.model.currentKey = cmd.displayKey
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package bitmaps
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/external"
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"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"
|
||||
)
|
||||
|
||||
type bitmapInfo struct {
|
||||
title string
|
||||
languageSpecific bool
|
||||
bitmapType bitmap.Type
|
||||
bitmapFlags bitmap.Flag
|
||||
}
|
||||
|
||||
var knownBitmapTypes = map[resource.ID]bitmapInfo{
|
||||
ids.MfdDataBitmaps: {title: "MFD Data Images", languageSpecific: true, bitmapType: bitmap.TypeCompressed8Bit, bitmapFlags: bitmap.FlagTransparent},
|
||||
ids.ObjectMaterialBitmaps: {title: "Object Materials", languageSpecific: false, bitmapType: bitmap.TypeFlat8Bit, bitmapFlags: 0},
|
||||
ids.ObjectTextureBitmaps: {title: "Object Textures", languageSpecific: false, bitmapType: bitmap.TypeFlat8Bit, bitmapFlags: 0},
|
||||
ids.IconBitmaps: {title: "Wall Icons", languageSpecific: false, bitmapType: bitmap.TypeCompressed8Bit, bitmapFlags: bitmap.FlagTransparent},
|
||||
ids.GraffitiBitmaps: {title: "Graffiti", languageSpecific: false, bitmapType: bitmap.TypeCompressed8Bit, bitmapFlags: bitmap.FlagTransparent},
|
||||
ids.ScreenTextures: {title: "Screens", languageSpecific: false, bitmapType: bitmap.TypeCompressed8Bit, bitmapFlags: 0},
|
||||
}
|
||||
|
||||
var knownBitmapTypesOrder = []resource.ID{
|
||||
ids.MfdDataBitmaps,
|
||||
ids.ObjectMaterialBitmaps,
|
||||
ids.ObjectTextureBitmaps,
|
||||
ids.IconBitmaps,
|
||||
ids.GraffitiBitmaps,
|
||||
ids.ScreenTextures,
|
||||
}
|
||||
|
||||
// View provides edit controls for bitmaps.
|
||||
type View struct {
|
||||
mod *world.Mod
|
||||
imageCache *graphics.TextureCache
|
||||
paletteCache *graphics.PaletteCache
|
||||
|
||||
modalStateMachine gui.ModalStateMachine
|
||||
clipboard external.Clipboard
|
||||
guiScale float32
|
||||
commander cmd.Commander
|
||||
|
||||
model viewModel
|
||||
}
|
||||
|
||||
// NewBitmapsView returns a new instance.
|
||||
func NewBitmapsView(mod *world.Mod, imageCache *graphics.TextureCache, paletteCache *graphics.PaletteCache,
|
||||
modalStateMachine gui.ModalStateMachine, clipboard external.Clipboard,
|
||||
guiScale float32, commander cmd.Commander) *View {
|
||||
view := &View{
|
||||
mod: mod,
|
||||
imageCache: imageCache,
|
||||
paletteCache: paletteCache,
|
||||
|
||||
modalStateMachine: modalStateMachine,
|
||||
clipboard: clipboard,
|
||||
guiScale: guiScale,
|
||||
commander: commander,
|
||||
|
||||
model: freshViewModel(),
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// WindowOpen returns the flag address, to be used with the main menu.
|
||||
func (view *View) WindowOpen() *bool {
|
||||
return &view.model.windowOpen
|
||||
}
|
||||
|
||||
// Render renders the view.
|
||||
func (view *View) Render() {
|
||||
if view.model.restoreFocus {
|
||||
imgui.SetNextWindowFocus()
|
||||
view.model.restoreFocus = false
|
||||
view.model.windowOpen = true
|
||||
}
|
||||
if view.model.windowOpen {
|
||||
imgui.SetNextWindowSizeV(imgui.Vec2{X: 800 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||
if imgui.BeginV("Bitmaps", view.WindowOpen(), imgui.WindowFlagsNoCollapse|imgui.WindowFlagsHorizontalScrollbar) {
|
||||
view.renderContent()
|
||||
}
|
||||
imgui.End()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) renderContent() {
|
||||
if imgui.BeginChildV("Properties", imgui.Vec2{X: 350 * view.guiScale, Y: 0}, false, 0) {
|
||||
imgui.PushItemWidth(-150 * view.guiScale)
|
||||
if imgui.BeginCombo("Bitmap Type", knownBitmapTypes[view.model.currentKey.ID].title) {
|
||||
for _, id := range knownBitmapTypesOrder {
|
||||
if imgui.SelectableV(knownBitmapTypes[id].title, id == view.model.currentKey.ID, 0, imgui.Vec2{}) {
|
||||
view.model.currentKey.ID = id
|
||||
view.model.currentKey.Index = 0
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
selectedType := knownBitmapTypes[view.model.currentKey.ID]
|
||||
if selectedType.languageSpecific {
|
||||
if imgui.BeginCombo("Language", view.model.currentKey.Lang.String()) {
|
||||
languages := resource.Languages()
|
||||
for _, lang := range languages {
|
||||
if imgui.SelectableV(lang.String(), lang == view.model.currentKey.Lang, 0, imgui.Vec2{}) {
|
||||
view.model.currentKey.Lang = lang
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
} else {
|
||||
view.model.currentKey.Lang = resource.LangAny
|
||||
}
|
||||
|
||||
info, _ := ids.Info(view.model.currentKey.ID)
|
||||
|
||||
gui.StepSliderInt("Index", &view.model.currentKey.Index, 0, info.MaxCount-1)
|
||||
|
||||
render.TextureSelector("###"+"IndexBitmap", -1, view.guiScale, info.MaxCount,
|
||||
view.model.currentKey.Index, view.imageCache,
|
||||
view.indexedResourceKey,
|
||||
func(index int) string { return fmt.Sprintf("%d", index) },
|
||||
func(newValue int) {
|
||||
view.model.currentKey.Index = newValue
|
||||
})
|
||||
|
||||
tex, err := view.imageCache.Texture(view.currentResourceKey())
|
||||
|
||||
if imgui.Button("Clear") {
|
||||
view.requestClear(selectedType)
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Import") {
|
||||
view.requestImport(selectedType)
|
||||
}
|
||||
if err == nil {
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Export") {
|
||||
view.requestExport()
|
||||
}
|
||||
if view.hasModCurrentBitmap() {
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Remove") {
|
||||
view.requestSetBitmapData(nil)
|
||||
}
|
||||
}
|
||||
|
||||
width, height := tex.Size()
|
||||
imgui.LabelText("Width", fmt.Sprintf("%d", int(width)))
|
||||
imgui.LabelText("Height", fmt.Sprintf("%d", int(height)))
|
||||
}
|
||||
|
||||
imgui.PopItemWidth()
|
||||
}
|
||||
imgui.EndChild()
|
||||
imgui.SameLine()
|
||||
render.TextureImage("Big texture", view.imageCache, view.currentResourceKey(), imgui.Vec2{X: 320 * view.guiScale, Y: 240 * view.guiScale})
|
||||
}
|
||||
|
||||
func (view *View) currentResourceKey() resource.Key {
|
||||
return view.indexedResourceKey(view.model.currentKey.Index)
|
||||
}
|
||||
|
||||
func (view *View) indexedResourceKey(index int) resource.Key {
|
||||
key := view.model.currentKey
|
||||
info, _ := ids.Info(view.model.currentKey.ID)
|
||||
if !info.List {
|
||||
key.ID = key.ID.Plus(index)
|
||||
key.Index = 0
|
||||
} else {
|
||||
key.Index = index
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func (view *View) hasModCurrentBitmap() bool {
|
||||
key := view.currentResourceKey()
|
||||
return len(view.mod.ModifiedBlock(key.Lang, key.ID, key.Index)) > 0
|
||||
}
|
||||
|
||||
func (view *View) requestExport() {
|
||||
key := view.currentResourceKey()
|
||||
texture, err := view.imageCache.Texture(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
palette, err := view.paletteCache.Palette(0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rawPalette := palette.Palette()
|
||||
filename := fmt.Sprintf("%05d_%03d_%s.png", key.ID.Value(), key.Index, key.Lang.String())
|
||||
width, height := texture.Size()
|
||||
bmp := bitmap.Bitmap{
|
||||
Header: bitmap.Header{
|
||||
Width: int16(width),
|
||||
Height: int16(height),
|
||||
},
|
||||
Pixels: texture.PixelData(),
|
||||
Palette: &rawPalette,
|
||||
}
|
||||
|
||||
external.ExportImage(view.modalStateMachine, filename, bmp)
|
||||
}
|
||||
|
||||
func (view *View) requestImport(bmpInfo bitmapInfo) {
|
||||
paletteRetriever := func() (bitmap.Palette, error) {
|
||||
palette, err := view.paletteCache.Palette(0)
|
||||
if err != nil {
|
||||
return bitmap.Palette{}, err
|
||||
}
|
||||
return palette.Palette(), nil
|
||||
}
|
||||
external.ImportImage(view.modalStateMachine, paletteRetriever, func(bmp bitmap.Bitmap) {
|
||||
view.requestSetBitmap(bmp, bmpInfo)
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) requestClear(bmpInfo bitmapInfo) {
|
||||
bmp := bitmap.Bitmap{
|
||||
Header: bitmap.Header{
|
||||
Width: 1,
|
||||
Height: 1,
|
||||
},
|
||||
Pixels: []byte{0x00},
|
||||
}
|
||||
view.requestSetBitmap(bmp, bmpInfo)
|
||||
}
|
||||
|
||||
func (view *View) requestSetBitmap(bmp bitmap.Bitmap, bmpInfo bitmapInfo) {
|
||||
highestBitShift := func(value int16) (result byte) {
|
||||
if value != 0 {
|
||||
for (value >> result) != 1 {
|
||||
result++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
bmp.Header.Flags = bmpInfo.bitmapFlags
|
||||
bmp.Header.Type = bmpInfo.bitmapType
|
||||
bmp.Header.WidthFactor = highestBitShift(bmp.Header.Width)
|
||||
bmp.Header.HeightFactor = highestBitShift(bmp.Header.Height)
|
||||
bmp.Header.Stride = uint16(bmp.Header.Width)
|
||||
data := bitmap.Encode(&bmp, 0)
|
||||
view.requestSetBitmapData(data)
|
||||
}
|
||||
|
||||
func (view *View) requestSetBitmapData(newData []byte) {
|
||||
resourceKey := view.currentResourceKey()
|
||||
|
||||
command := setBitmapCommand{
|
||||
displayKey: view.model.currentKey,
|
||||
model: &view.model,
|
||||
|
||||
resourceKey: resourceKey,
|
||||
oldData: view.mod.ModifiedBlock(resourceKey.Lang, resourceKey.ID, resourceKey.Index),
|
||||
newData: newData,
|
||||
}
|
||||
view.commander.Queue(command)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package bitmaps
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
type viewModel struct {
|
||||
windowOpen bool
|
||||
restoreFocus bool
|
||||
|
||||
currentKey resource.Key
|
||||
}
|
||||
|
||||
func freshViewModel() viewModel {
|
||||
return viewModel{
|
||||
currentKey: resource.KeyOf(ids.MfdDataBitmaps, resource.LangDefault, 0),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package editor and its sub-packages contain the editor-specifics of hacked.
|
||||
*/
|
||||
package editor
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package external
|
||||
|
||||
// Clipboard represents a temporary storage.
|
||||
type Clipboard interface {
|
||||
// String returns the current value of the clipboard, if it is compatible with UTF-8.
|
||||
String() (string, error)
|
||||
// SetString sets the current value of the clipboard as UTF-8 string.
|
||||
SetString(value string)
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/audio"
|
||||
"github.com/inkyblackness/hacked/ss1/content/audio/wav"
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
// Export starts an export dialog series, calling the given callback with a folder name.
|
||||
func Export(machine gui.ModalStateMachine, info string, callback func(string), lastFailed bool) {
|
||||
machine.SetState(&exportStartState{
|
||||
machine: machine,
|
||||
callback: callback,
|
||||
info: info,
|
||||
withError: lastFailed,
|
||||
})
|
||||
}
|
||||
|
||||
// ExportAudio is a helper wrapper for exporting audio.
|
||||
func ExportAudio(machine gui.ModalStateMachine, filename string, sound audio.L8) {
|
||||
info := "File to be written: " + filename
|
||||
var dirHandler func(string)
|
||||
|
||||
dirHandler = func(dirname string) {
|
||||
writer, err := os.Create(filepath.Join(dirname, filename))
|
||||
if err != nil {
|
||||
Export(machine, info, dirHandler, true)
|
||||
return
|
||||
}
|
||||
defer func() { _ = writer.Close() }()
|
||||
err = wav.Save(writer, sound.SampleRate, sound.Samples)
|
||||
if err != nil {
|
||||
Export(machine, info, dirHandler, true)
|
||||
}
|
||||
}
|
||||
|
||||
Export(machine, info, dirHandler, false)
|
||||
}
|
||||
|
||||
// ExportImage is a helper wrapper for exporting single images.
|
||||
func ExportImage(machine gui.ModalStateMachine, filename string, bmp bitmap.Bitmap) {
|
||||
info := "File to be written: " + filename
|
||||
var exportTo func(string)
|
||||
|
||||
exportTo = func(dirname string) {
|
||||
writer, err := os.Create(filepath.Join(dirname, filename))
|
||||
if err != nil {
|
||||
Export(machine, "Could not create file.\n"+info, exportTo, true)
|
||||
return
|
||||
}
|
||||
defer func() { _ = writer.Close() }()
|
||||
|
||||
imageRect := image.Rect(0, 0, int(bmp.Header.Width), int(bmp.Header.Height))
|
||||
imagePal := bmp.Palette.ColorPalette(false)
|
||||
paletted := image.NewPaletted(imageRect, imagePal)
|
||||
paletted.Pix = bmp.Pixels
|
||||
err = png.Encode(writer, paletted)
|
||||
if err != nil {
|
||||
Export(machine, info, exportTo, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Export(machine, info, exportTo, false)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
type exportStartState struct {
|
||||
machine gui.ModalStateMachine
|
||||
callback func(string)
|
||||
info string
|
||||
withError bool
|
||||
}
|
||||
|
||||
func (state exportStartState) Render() {
|
||||
imgui.OpenPopup("Export")
|
||||
nextState := &exportWaitingState{
|
||||
machine: state.machine,
|
||||
callback: state.callback,
|
||||
info: state.info,
|
||||
}
|
||||
if state.withError {
|
||||
nextState.failureTime = time.Now()
|
||||
}
|
||||
state.machine.SetState(nextState)
|
||||
}
|
||||
|
||||
func (state exportStartState) HandleFiles(names []string) {
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
"github.com/sqweek/dialog"
|
||||
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
type exportWaitingState struct {
|
||||
machine gui.ModalStateMachine
|
||||
callback func(string)
|
||||
info string
|
||||
|
||||
failureTime time.Time
|
||||
}
|
||||
|
||||
func (state *exportWaitingState) Render() {
|
||||
if imgui.BeginPopupModalV("Export", 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, could not save file.\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
|
||||
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("Saving will potentially overwrite existing files in the folder.")
|
||||
imgui.PopStyleColor()
|
||||
imgui.Text(state.info)
|
||||
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 *exportWaitingState) HandleFiles(names []string) {
|
||||
dirPath, ok := state.verifyDir(names)
|
||||
if ok {
|
||||
state.machine.SetState(nil)
|
||||
state.callback(dirPath)
|
||||
} else {
|
||||
state.failureTime = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func (state exportWaitingState) 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
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
"github.com/sqweek/dialog"
|
||||
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
// SingleFileHandler is a callback for a single selected file.
|
||||
type SingleFileHandler func(string) error
|
||||
|
||||
type singleFileRunner func(*dialog.FileBuilder) (string, error)
|
||||
|
||||
// SaveFile starts a dialog series to save a file.
|
||||
func SaveFile(machine gui.ModalStateMachine, types []TypeInfo, callback SingleFileHandler) {
|
||||
machine.SetState(&fileStartState{
|
||||
machine: machine,
|
||||
title: "Save",
|
||||
typeInfo: types,
|
||||
runner: func(builder *dialog.FileBuilder) (string, error) { return builder.Save() },
|
||||
callback: callback,
|
||||
})
|
||||
}
|
||||
|
||||
// LoadFile starts a dialog series to load a file.
|
||||
func LoadFile(machine gui.ModalStateMachine, types []TypeInfo, callback SingleFileHandler) {
|
||||
machine.SetState(&fileStartState{
|
||||
machine: machine,
|
||||
title: "Load",
|
||||
typeInfo: types,
|
||||
runner: func(builder *dialog.FileBuilder) (string, error) { return builder.Load() },
|
||||
callback: callback,
|
||||
})
|
||||
}
|
||||
|
||||
type fileStartState struct {
|
||||
machine gui.ModalStateMachine
|
||||
title string
|
||||
typeInfo []TypeInfo
|
||||
runner singleFileRunner
|
||||
callback SingleFileHandler
|
||||
}
|
||||
|
||||
func (state fileStartState) HandleFiles(names []string) {
|
||||
}
|
||||
|
||||
func (state fileStartState) Render() {
|
||||
imgui.OpenPopup(state.title)
|
||||
nextState := &fileWaitingState{
|
||||
machine: state.machine,
|
||||
title: state.title,
|
||||
typeInfo: state.typeInfo,
|
||||
runner: state.runner,
|
||||
callback: state.callback,
|
||||
}
|
||||
state.machine.SetState(nextState)
|
||||
}
|
||||
|
||||
type fileWaitingState struct {
|
||||
machine gui.ModalStateMachine
|
||||
title string
|
||||
typeInfo []TypeInfo
|
||||
runner singleFileRunner
|
||||
callback SingleFileHandler
|
||||
|
||||
renderCount int
|
||||
shouldOpenDialog bool
|
||||
problem string
|
||||
}
|
||||
|
||||
func (state *fileWaitingState) HandleFiles(names []string) {
|
||||
}
|
||||
|
||||
func (state *fileWaitingState) Render() {
|
||||
if imgui.BeginPopupModalV(state.title, nil,
|
||||
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||
if len(state.problem) == 0 {
|
||||
imgui.Text("Please use popup dialog...")
|
||||
} else {
|
||||
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
|
||||
imgui.Text(state.problem)
|
||||
imgui.PopStyleColor()
|
||||
}
|
||||
state.renderControls()
|
||||
imgui.EndPopup()
|
||||
|
||||
state.renderCount++
|
||||
if state.renderCount == 3 {
|
||||
state.shouldOpenDialog = true
|
||||
}
|
||||
|
||||
if state.shouldOpenDialog {
|
||||
state.shouldOpenDialog = false
|
||||
state.openDialog()
|
||||
}
|
||||
} else {
|
||||
state.machine.SetState(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (state *fileWaitingState) renderControls() {
|
||||
imgui.Separator()
|
||||
if imgui.Button("Browse...") {
|
||||
state.shouldOpenDialog = true
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Cancel") {
|
||||
state.machine.SetState(nil)
|
||||
imgui.CloseCurrentPopup()
|
||||
}
|
||||
}
|
||||
|
||||
func (state *fileWaitingState) openDialog() {
|
||||
dlgBuilder := dialog.File()
|
||||
for _, info := range state.typeInfo {
|
||||
dlgBuilder = dlgBuilder.Filter(info.Title, info.Extensions...)
|
||||
}
|
||||
dlgBuilder = dlgBuilder.Filter("All files (*.*)", "*")
|
||||
|
||||
filename, err := state.runner(dlgBuilder)
|
||||
state.machine.SetState(nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
state.reportFile(filename)
|
||||
}
|
||||
|
||||
func (state *fileWaitingState) reportFile(filename string) {
|
||||
state.problem = ""
|
||||
|
||||
err := state.callback(filename)
|
||||
if err != nil {
|
||||
state.problem = "Previous attempt failed.\nPlease check and try again.\nReason:\n" + err.Error()
|
||||
state.machine.SetState(state)
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/audio"
|
||||
"github.com/inkyblackness/hacked/ss1/content/audio/wav"
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
// Import starts an import dialog series, calling the given callback with a file name.
|
||||
func Import(machine gui.ModalStateMachine, info string, types []TypeInfo, callback func(string), lastFailed bool) {
|
||||
machine.SetState(&importStartState{
|
||||
machine: machine,
|
||||
callback: callback,
|
||||
info: info,
|
||||
typeInfo: types,
|
||||
withError: lastFailed,
|
||||
})
|
||||
}
|
||||
|
||||
// ImportAudio is a helper to handle audio file import. The callback is called with the loaded audio.
|
||||
func ImportAudio(machine gui.ModalStateMachine, callback func(l8 audio.L8)) {
|
||||
info := "File must be a WAV file, 22050 Hz, 8-bit or 16-bit, uncompressed."
|
||||
types := []TypeInfo{{Title: "Audio files (*.wav)", Extensions: []string{"wav"}}}
|
||||
var fileHandler func(string)
|
||||
|
||||
fileHandler = func(filename string) {
|
||||
reader, err := os.Open(filename)
|
||||
if err != nil {
|
||||
Import(machine, info, types, fileHandler, true)
|
||||
return
|
||||
}
|
||||
defer func() { _ = reader.Close() }()
|
||||
sound, err := wav.Load(reader)
|
||||
if err != nil {
|
||||
Import(machine, info, types, fileHandler, true)
|
||||
return
|
||||
}
|
||||
callback(sound)
|
||||
}
|
||||
|
||||
Import(machine, info, types, fileHandler, false)
|
||||
}
|
||||
|
||||
// ImportImage is a helper to handle image file import. The callback is called with the loaded image.
|
||||
func ImportImage(machine gui.ModalStateMachine, paletteRetriever func() (bitmap.Palette, error), callback func(bitmap.Bitmap)) {
|
||||
info := "File should be either a BMP, GIF, or a PNG file.\nPaletted images matching game palette are taken 1:1,\nothers are mapped closest fitting."
|
||||
types := []TypeInfo{{Title: "Image files (*.bmp, *.gif, *.png)", Extensions: []string{"bmp", "gif", "png"}}}
|
||||
var fileHandler func(string)
|
||||
|
||||
fileHandler = func(filename string) {
|
||||
reader, err := os.Open(filename)
|
||||
if err != nil {
|
||||
Import(machine, "Could not open file.\n"+info, types, fileHandler, true)
|
||||
return
|
||||
}
|
||||
defer func() { _ = reader.Close() }()
|
||||
img, _, err := image.Decode(reader)
|
||||
if err != nil {
|
||||
Import(machine, "File not recognized as image.\n"+info, types, fileHandler, true)
|
||||
return
|
||||
}
|
||||
|
||||
var bmp bitmap.Bitmap
|
||||
importMapped := true
|
||||
rawPalette, err := paletteRetriever()
|
||||
if err != nil {
|
||||
Import(machine, "Can not import image without having a palette loaded.\n"+info, types, fileHandler, true)
|
||||
return
|
||||
}
|
||||
if palettedImg, isPaletted := img.(image.PalettedImage); isPaletted {
|
||||
imgPalette, hasPalette := palettedImg.ColorModel().(color.Palette)
|
||||
if hasPalette && paletteMatches(imgPalette, rawPalette.ColorPalette(false)) {
|
||||
bounds := img.Bounds()
|
||||
|
||||
bmp.Header.Width = int16(math.Max(0, math.Min(float64(bounds.Dx()), math.MaxInt16)))
|
||||
bmp.Header.Height = int16(math.Max(0, math.Min(float64(bounds.Dy()), math.MaxInt16)))
|
||||
bmp.Pixels = make([]byte, int(bmp.Header.Width)*int(bmp.Header.Height))
|
||||
for row := 0; row < int(bmp.Header.Height); row++ {
|
||||
for column := 0; column < int(bmp.Header.Width); column++ {
|
||||
bmp.Pixels[row*int(bmp.Header.Width)+column] = palettedImg.ColorIndexAt(column, row)
|
||||
}
|
||||
}
|
||||
importMapped = false
|
||||
}
|
||||
}
|
||||
if importMapped {
|
||||
bitmapper := bitmap.NewBitmapper(&rawPalette)
|
||||
bmp = bitmapper.Map(img)
|
||||
}
|
||||
callback(bmp)
|
||||
}
|
||||
|
||||
Import(machine, info, types, fileHandler, false)
|
||||
}
|
||||
|
||||
func paletteMatches(imgPalette color.Palette, rawPalette color.Palette) bool {
|
||||
if len(imgPalette) > len(rawPalette) {
|
||||
return false
|
||||
}
|
||||
|
||||
for index, clr := range imgPalette {
|
||||
imgR, imgG, imgB, _ := clr.RGBA()
|
||||
rawR, rawG, rawB, _ := rawPalette[index].RGBA()
|
||||
|
||||
if (imgR != rawR) || (imgG != rawG) || (imgB != rawB) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
type importStartState struct {
|
||||
machine gui.ModalStateMachine
|
||||
callback func(string)
|
||||
info string
|
||||
typeInfo []TypeInfo
|
||||
withError bool
|
||||
}
|
||||
|
||||
func (state importStartState) Render() {
|
||||
imgui.OpenPopup("Import")
|
||||
nextState := &importWaitingState{
|
||||
machine: state.machine,
|
||||
callback: state.callback,
|
||||
info: state.info,
|
||||
typeInfo: state.typeInfo,
|
||||
}
|
||||
if state.withError {
|
||||
nextState.failureTime = time.Now()
|
||||
}
|
||||
state.machine.SetState(nextState)
|
||||
}
|
||||
|
||||
func (state importStartState) HandleFiles(names []string) {
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package external
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
"github.com/sqweek/dialog"
|
||||
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
type importWaitingState struct {
|
||||
machine gui.ModalStateMachine
|
||||
callback func(string)
|
||||
info string
|
||||
typeInfo []TypeInfo
|
||||
|
||||
failureTime time.Time
|
||||
}
|
||||
|
||||
func (state *importWaitingState) Render() {
|
||||
if imgui.BeginPopupModalV("Import", nil,
|
||||
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||
imgui.Text("Waiting for file.")
|
||||
if !state.failureTime.IsZero() {
|
||||
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
|
||||
imgui.Text("Previous attempt failed, could not load file.\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 file
|
||||
that shall be loaded into the editor window.
|
||||
`)
|
||||
imgui.Text(state.info)
|
||||
imgui.Separator()
|
||||
if imgui.Button("Browse...") {
|
||||
dlgBuilder := dialog.File()
|
||||
for _, info := range state.typeInfo {
|
||||
dlgBuilder = dlgBuilder.Filter(info.Title, info.Extensions...)
|
||||
}
|
||||
dlgBuilder = dlgBuilder.Filter("All files (*.*)", "*")
|
||||
filename, err := dlgBuilder.Load()
|
||||
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 *importWaitingState) HandleFiles(names []string) {
|
||||
filename, ok := state.verifyFile(names)
|
||||
if ok {
|
||||
state.machine.SetState(nil)
|
||||
state.callback(filename)
|
||||
} else {
|
||||
state.failureTime = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func (state importWaitingState) verifyFile(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
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package external
|
||||
|
||||
// TypeInfo describes a file.
|
||||
type TypeInfo struct {
|
||||
Title string
|
||||
Extensions []string
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package graphics
|
||||
|
||||
import "github.com/inkyblackness/hacked/ui/opengl"
|
||||
|
||||
// BitmapTexture wraps an OpenGL handle for a downloaded image.
|
||||
type BitmapTexture struct {
|
||||
gl opengl.OpenGL
|
||||
handle uint32
|
||||
|
||||
width, height float32
|
||||
u, v float32
|
||||
|
||||
pixelData []byte
|
||||
}
|
||||
|
||||
func powerOfTwo(value int) int {
|
||||
result := 2
|
||||
|
||||
for (result < value) && (result < 0x1000) {
|
||||
result *= 2
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// NewBitmapTexture downloads the provided raw data to OpenGL and returns a BitmapTexture instance.
|
||||
func NewBitmapTexture(gl opengl.OpenGL, width, height int, pixelData []byte) *BitmapTexture {
|
||||
textureWidth := powerOfTwo(width)
|
||||
textureHeight := powerOfTwo(height)
|
||||
tex := &BitmapTexture{
|
||||
gl: gl,
|
||||
handle: gl.GenTextures(1)[0],
|
||||
|
||||
width: float32(width),
|
||||
height: float32(height),
|
||||
|
||||
pixelData: pixelData,
|
||||
}
|
||||
tex.u = tex.width / float32(textureWidth)
|
||||
tex.v = tex.height / float32(textureHeight)
|
||||
|
||||
paddedData := make([]byte, textureWidth*textureHeight)
|
||||
for y := 0; y < height; y++ {
|
||||
inStart := y * width
|
||||
outOffset := y * textureWidth
|
||||
for x := 0; x < width; x++ {
|
||||
value := pixelData[inStart+x]
|
||||
paddedData[outOffset] = value
|
||||
outOffset++
|
||||
}
|
||||
}
|
||||
|
||||
gl.BindTexture(opengl.TEXTURE_2D, tex.handle)
|
||||
gl.TexImage2D(opengl.TEXTURE_2D, 0, opengl.RED, int32(textureWidth), int32(textureHeight),
|
||||
0, opengl.RED, opengl.UNSIGNED_BYTE, paddedData)
|
||||
gl.TexParameteri(opengl.TEXTURE_2D, opengl.TEXTURE_MAG_FILTER, opengl.NEAREST)
|
||||
gl.TexParameteri(opengl.TEXTURE_2D, opengl.TEXTURE_MIN_FILTER, opengl.NEAREST)
|
||||
gl.GenerateMipmap(opengl.TEXTURE_2D)
|
||||
gl.BindTexture(opengl.TEXTURE_2D, 0)
|
||||
|
||||
return tex
|
||||
}
|
||||
|
||||
// Dispose releases the OpenGL texture.
|
||||
func (tex *BitmapTexture) Dispose() {
|
||||
if tex.handle != 0 {
|
||||
tex.gl.DeleteTextures([]uint32{tex.handle})
|
||||
tex.handle = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Handle returns the texture handle.
|
||||
func (tex *BitmapTexture) Handle() uint32 {
|
||||
return tex.handle
|
||||
}
|
||||
|
||||
// PixelData returns the original paletted pixel data.
|
||||
func (tex *BitmapTexture) PixelData() []byte {
|
||||
return tex.pixelData
|
||||
}
|
||||
|
||||
// Size returns the dimensions of the bitmap, in pixels.
|
||||
func (tex *BitmapTexture) Size() (width, height float32) {
|
||||
return tex.width, tex.height
|
||||
}
|
||||
|
||||
// UV returns the maximum U and V values for the bitmap. The bitmap will be
|
||||
// stored in a power-of-two texture, which may be larger than the bitmap.
|
||||
func (tex *BitmapTexture) UV() (u, v float32) {
|
||||
return tex.u, tex.v
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package graphics
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
)
|
||||
|
||||
// FrameCacheKey is used to identify a cached frame.
|
||||
type FrameCacheKey uint16
|
||||
|
||||
// FrameCache loads bitmaps with their palettes and provides OpenGL textures.
|
||||
type FrameCache struct {
|
||||
gl opengl.OpenGL
|
||||
|
||||
textures map[FrameCacheKey]*BitmapTexture
|
||||
palettes map[FrameCacheKey]*PaletteTexture
|
||||
|
||||
keyCounter uint16
|
||||
}
|
||||
|
||||
// NewFrameCache returns a new instance.
|
||||
func NewFrameCache(gl opengl.OpenGL) *FrameCache {
|
||||
cache := &FrameCache{
|
||||
gl: gl,
|
||||
textures: make(map[FrameCacheKey]*BitmapTexture),
|
||||
palettes: make(map[FrameCacheKey]*PaletteTexture),
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
// AllocateKey returns a new key to be used with uploading a new frame.
|
||||
func (cache *FrameCache) AllocateKey() FrameCacheKey {
|
||||
cache.keyCounter++
|
||||
return FrameCacheKey(cache.keyCounter)
|
||||
}
|
||||
|
||||
// SetTexture registers a texture based on given bitmap under given key.
|
||||
// The bitmap should contain a palette, otherwise it will most likely not be displayed.
|
||||
func (cache *FrameCache) SetTexture(key FrameCacheKey, width, height uint16, pixels []byte, palette *bitmap.Palette) {
|
||||
cache.DropTextureForKey(key)
|
||||
tex := NewBitmapTexture(cache.gl, int(width), int(height), pixels)
|
||||
cache.textures[key] = tex
|
||||
if palette == nil {
|
||||
return
|
||||
}
|
||||
pal := NewPaletteTexture(cache.gl, palette)
|
||||
cache.palettes[key] = pal
|
||||
}
|
||||
|
||||
// DropTextureForKey removes the currently cached frame.
|
||||
func (cache *FrameCache) DropTextureForKey(key FrameCacheKey) {
|
||||
if tex, existing := cache.textures[key]; existing {
|
||||
tex.Dispose()
|
||||
delete(cache.textures, key)
|
||||
}
|
||||
if pal, existing := cache.palettes[key]; existing {
|
||||
pal.Dispose()
|
||||
delete(cache.palettes, key)
|
||||
}
|
||||
}
|
||||
|
||||
// HandlesForKey returns the OpenGL handles for both palette and texture for given key.
|
||||
func (cache FrameCache) HandlesForKey(key FrameCacheKey) (palette uint32, texture uint32) {
|
||||
if tex, existing := cache.textures[key]; existing {
|
||||
texture = tex.Handle()
|
||||
}
|
||||
if pal, existing := cache.palettes[key]; existing {
|
||||
palette = pal.Handle()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Texture returns the buffered texture for given key.
|
||||
func (cache *FrameCache) Texture(key FrameCacheKey) *BitmapTexture {
|
||||
return cache.textures[key]
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package graphics
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
)
|
||||
|
||||
// PaletteCache loads palettes and provides OpenGL textures.
|
||||
type PaletteCache struct {
|
||||
gl opengl.OpenGL
|
||||
localizer resource.Localizer
|
||||
|
||||
palettes map[resource.Key]*PaletteTexture
|
||||
}
|
||||
|
||||
// NewPaletteCache returns a new instance.
|
||||
func NewPaletteCache(gl opengl.OpenGL, localizer resource.Localizer) *PaletteCache {
|
||||
cache := &PaletteCache{
|
||||
gl: gl,
|
||||
localizer: localizer,
|
||||
palettes: make(map[resource.Key]*PaletteTexture),
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
// InvalidateResources lets the cache remove any palette from resources that are specified in the given slice.
|
||||
func (cache *PaletteCache) InvalidateResources(ids []resource.ID) {
|
||||
for _, id := range ids {
|
||||
for key, texture := range cache.palettes {
|
||||
if key.ID == id {
|
||||
texture.Dispose()
|
||||
delete(cache.palettes, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Palette returns the palette with given index - if available.
|
||||
func (cache *PaletteCache) Palette(index int) (*PaletteTexture, error) {
|
||||
key := resource.KeyOf(ids.GamePalettesStart.Plus(index), resource.LangAny, 0)
|
||||
pal, existing := cache.palettes[key]
|
||||
if existing {
|
||||
return pal, nil
|
||||
}
|
||||
selector := cache.localizer.LocalizedResources(key.Lang)
|
||||
view, err := selector.Select(key.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if view.ContentType() != resource.Palette {
|
||||
return nil, resource.ErrWrongType(key, resource.Palette)
|
||||
}
|
||||
reader, err := view.Block(key.Index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var palette bitmap.Palette
|
||||
err = binary.Read(reader, binary.LittleEndian, &palette)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pal = NewPaletteTexture(cache.gl, &palette)
|
||||
cache.palettes[key] = pal
|
||||
|
||||
return pal, nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package graphics
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
)
|
||||
|
||||
// PaletteTexture contains a palette stored as OpenGL texture.
|
||||
type PaletteTexture struct {
|
||||
gl opengl.OpenGL
|
||||
|
||||
handle uint32
|
||||
palette bitmap.Palette
|
||||
}
|
||||
|
||||
// NewPaletteTexture creates a new PaletteTexture instance.
|
||||
func NewPaletteTexture(gl opengl.OpenGL, palette *bitmap.Palette) *PaletteTexture {
|
||||
tex := &PaletteTexture{
|
||||
gl: gl,
|
||||
handle: gl.GenTextures(1)[0],
|
||||
}
|
||||
|
||||
tex.Update(palette)
|
||||
|
||||
return tex
|
||||
}
|
||||
|
||||
// Dispose releases the OpenGL texture.
|
||||
func (tex *PaletteTexture) Dispose() {
|
||||
if tex.handle != 0 {
|
||||
tex.gl.DeleteTextures([]uint32{tex.handle})
|
||||
tex.handle = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Palette returns the original palette.
|
||||
func (tex *PaletteTexture) Palette() bitmap.Palette {
|
||||
return tex.palette
|
||||
}
|
||||
|
||||
// Handle returns the texture handle.
|
||||
func (tex *PaletteTexture) Handle() uint32 {
|
||||
return tex.handle
|
||||
}
|
||||
|
||||
// Update reloads the palette.
|
||||
func (tex *PaletteTexture) Update(palette *bitmap.Palette) {
|
||||
gl := tex.gl
|
||||
const bytesPerRGBA = 4
|
||||
const colors = 256
|
||||
var data [colors * bytesPerRGBA]byte
|
||||
|
||||
for i := 0; i < colors; i++ {
|
||||
entry := palette[i]
|
||||
data[i*bytesPerRGBA+0] = entry.Red
|
||||
data[i*bytesPerRGBA+1] = entry.Green
|
||||
data[i*bytesPerRGBA+2] = entry.Blue
|
||||
data[i*bytesPerRGBA+3] = 0xFF
|
||||
}
|
||||
data[3] = 0x00
|
||||
|
||||
gl.BindTexture(opengl.TEXTURE_2D, tex.handle)
|
||||
gl.TexImage2D(opengl.TEXTURE_2D, 0, opengl.RGBA, int32(colors), 1, 0, opengl.RGBA, opengl.UNSIGNED_BYTE, data[:])
|
||||
gl.TexParameteri(opengl.TEXTURE_2D, opengl.TEXTURE_MAG_FILTER, opengl.NEAREST)
|
||||
gl.TexParameteri(opengl.TEXTURE_2D, opengl.TEXTURE_MIN_FILTER, opengl.NEAREST)
|
||||
gl.GenerateMipmap(opengl.TEXTURE_2D)
|
||||
gl.BindTexture(opengl.TEXTURE_2D, 0)
|
||||
|
||||
tex.palette = *palette
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package graphics
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1"
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
)
|
||||
|
||||
const (
|
||||
errReferenceNotFound ss1.StringError = "reference not existing"
|
||||
errReferenceWrongDimensions ss1.StringError = "reference has wrong dimensions"
|
||||
)
|
||||
|
||||
// TextureCache loads bitmaps and provides OpenGL textures.
|
||||
type TextureCache struct {
|
||||
gl opengl.OpenGL
|
||||
localizer resource.Localizer
|
||||
|
||||
textures map[resource.Key]*BitmapTexture
|
||||
}
|
||||
|
||||
// NewTextureCache returns a new instance.
|
||||
func NewTextureCache(gl opengl.OpenGL, localizer resource.Localizer) *TextureCache {
|
||||
cache := &TextureCache{
|
||||
gl: gl,
|
||||
localizer: localizer,
|
||||
textures: make(map[resource.Key]*BitmapTexture),
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
// InvalidateResources lets the cache remove any textures from resources that are specified in the given slice.
|
||||
func (cache *TextureCache) InvalidateResources(ids []resource.ID) {
|
||||
for _, id := range ids {
|
||||
for key, texture := range cache.textures {
|
||||
if key.ID == id {
|
||||
texture.Dispose()
|
||||
delete(cache.textures, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Texture returns the texture with given key - if available.
|
||||
func (cache *TextureCache) Texture(key resource.Key) (*BitmapTexture, error) {
|
||||
return cache.TextureReferenced(key, nil)
|
||||
}
|
||||
|
||||
// TextureReferenced returns the texture with given key - if available.
|
||||
// Should the underlying bitmap have to be loaded, then the given reference is taken as a basis for compressed bitmaps.
|
||||
// If reference is nil, then no reference is used.
|
||||
func (cache *TextureCache) TextureReferenced(key resource.Key, reference *resource.Key) (*BitmapTexture, error) {
|
||||
tex, existing := cache.textures[key]
|
||||
if existing {
|
||||
return tex, nil
|
||||
}
|
||||
selector := cache.localizer.LocalizedResources(key.Lang)
|
||||
view, err := selector.Select(key.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if view.ContentType() != resource.Bitmap {
|
||||
return nil, resource.ErrWrongType(key, resource.Bitmap)
|
||||
}
|
||||
reader, err := view.Block(key.Index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bmp, err := bitmap.DecodeReferenced(reader, func(width, height int16) ([]byte, error) {
|
||||
buf := make([]byte, int(width)*int(height))
|
||||
if reference == nil {
|
||||
return buf, nil
|
||||
}
|
||||
refTex, refExisting := cache.textures[*reference]
|
||||
if !refExisting {
|
||||
return nil, errReferenceNotFound
|
||||
}
|
||||
refWidth, refHeight := refTex.Size()
|
||||
if (int16(refWidth) != width) || (int16(refHeight) != height) {
|
||||
return nil, errReferenceWrongDimensions
|
||||
}
|
||||
copy(buf, refTex.PixelData())
|
||||
return buf, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tex = NewBitmapTexture(cache.gl, int(bmp.Header.Width), int(bmp.Header.Height), bmp.Pixels)
|
||||
cache.textures[key] = tex
|
||||
|
||||
return tex, nil
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
)
|
||||
|
||||
var gridVertexShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
in vec3 vertexPosition;
|
||||
|
||||
uniform mat4 viewMatrix;
|
||||
uniform mat4 projectionMatrix;
|
||||
|
||||
out vec4 gridColor;
|
||||
out vec3 originalPosition;
|
||||
|
||||
void main(void) {
|
||||
gl_Position = projectionMatrix * viewMatrix * vec4(vertexPosition, 1.0);
|
||||
|
||||
gridColor = vec4(0.0, 0.1, 0.0, 0.6);
|
||||
originalPosition = vertexPosition;
|
||||
}
|
||||
`
|
||||
|
||||
var gridFragmentShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
uniform vec4 gridSize;
|
||||
|
||||
in vec4 gridColor;
|
||||
in vec3 originalPosition;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
float modulo(float x, float y) {
|
||||
return x - y * floor(x/y);
|
||||
}
|
||||
|
||||
float nearGrid(float stepSize, float value) {
|
||||
float remainder = modulo(value - (stepSize / 2.0), stepSize) * 2.0;
|
||||
|
||||
if (remainder >= stepSize) {
|
||||
remainder = (stepSize * 2.0) - remainder;
|
||||
}
|
||||
|
||||
return remainder / stepSize;
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
float alphaX = nearGrid(256.0, originalPosition.x);
|
||||
float alphaY = nearGrid(256.0, originalPosition.y);
|
||||
bool beyondX = (originalPosition.x / 256.0) >= gridSize.x || (originalPosition.x < 0.0);
|
||||
bool beyondY = (originalPosition.y / 256.0) >= gridSize.y || (originalPosition.y < 0.0);
|
||||
float alpha = 0.0;
|
||||
|
||||
if (!beyondX && !beyondY) {
|
||||
alpha = max(alphaX, alphaY);
|
||||
} else if (beyondX && !beyondY) {
|
||||
alpha = alphaX;
|
||||
} else if (beyondY && !beyondX) {
|
||||
alpha = alphaY;
|
||||
} else {
|
||||
alpha = min(alphaX, alphaY);
|
||||
}
|
||||
|
||||
alpha = pow(2.0, 10.0 * (alpha - 1.0));
|
||||
|
||||
fragColor = vec4(gridColor.rgb, gridColor.a * alpha);
|
||||
}
|
||||
`
|
||||
|
||||
// BackgroundGrid renders a grid with transparent holes.
|
||||
type BackgroundGrid struct {
|
||||
context *render.Context
|
||||
|
||||
program uint32
|
||||
vao *opengl.VertexArrayObject
|
||||
vertexPositionBuffer uint32
|
||||
vertexPositionAttrib int32
|
||||
gridSizeUniform opengl.Vector4Uniform
|
||||
viewMatrixUniform opengl.Matrix4Uniform
|
||||
projectionMatrixUniform opengl.Matrix4Uniform
|
||||
|
||||
lastReportedColumns int
|
||||
lastReportedRows int
|
||||
}
|
||||
|
||||
// NewBackgroundGrid returns a new instance of BackgroundGrid.
|
||||
func NewBackgroundGrid(context *render.Context) *BackgroundGrid {
|
||||
gl := context.OpenGL
|
||||
program, programErr := opengl.LinkNewStandardProgram(gl, gridVertexShaderSource, gridFragmentShaderSource)
|
||||
if programErr != nil {
|
||||
panic(opengl.NamedShaderError{Name: "BackgroundGridShader", Nested: programErr})
|
||||
}
|
||||
grid := &BackgroundGrid{
|
||||
context: context,
|
||||
program: program,
|
||||
vao: opengl.NewVertexArrayObject(gl, program),
|
||||
vertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||
vertexPositionAttrib: gl.GetAttribLocation(program, "vertexPosition"),
|
||||
viewMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "viewMatrix")),
|
||||
projectionMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "projectionMatrix")),
|
||||
gridSizeUniform: opengl.Vector4Uniform(gl.GetUniformLocation(program, "gridSize")),
|
||||
}
|
||||
|
||||
return grid
|
||||
}
|
||||
|
||||
// Render renders the grid.
|
||||
func (grid *BackgroundGrid) Render(columns, rows int) {
|
||||
gl := grid.context.OpenGL
|
||||
|
||||
grid.setGridSize(columns, rows)
|
||||
|
||||
grid.vao.OnShader(func() {
|
||||
grid.viewMatrixUniform.Set(gl, grid.context.ViewMatrix)
|
||||
grid.projectionMatrixUniform.Set(gl, &grid.context.ProjectionMatrix)
|
||||
grid.gridSizeUniform.Set(gl, &[4]float32{float32(columns), float32(rows), 0, 0})
|
||||
|
||||
gl.DrawArrays(opengl.TRIANGLES, 0, 6)
|
||||
})
|
||||
}
|
||||
|
||||
func (grid *BackgroundGrid) setGridSize(columns, rows int) {
|
||||
if (grid.lastReportedColumns == columns) && (grid.lastReportedRows == rows) {
|
||||
return
|
||||
}
|
||||
grid.lastReportedColumns = columns
|
||||
grid.lastReportedRows = rows
|
||||
|
||||
gl := grid.context.OpenGL
|
||||
{
|
||||
hTiles := float32(columns)
|
||||
vTiles := float32(rows)
|
||||
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, grid.vertexPositionBuffer)
|
||||
fineHalf := level.FineCoordinatesPerTileSide / float32(2.0)
|
||||
hLimit := level.FineCoordinatesPerTileSide*hTiles + fineHalf
|
||||
vLimit := level.FineCoordinatesPerTileSide*vTiles + fineHalf
|
||||
var vertices = []float32{
|
||||
-fineHalf, -fineHalf, 0.0,
|
||||
hLimit, -fineHalf, 0.0,
|
||||
hLimit, vLimit, 0.0,
|
||||
|
||||
hLimit, vLimit, 0.0,
|
||||
-fineHalf, vLimit, 0.0,
|
||||
-fineHalf, -fineHalf, 0.0}
|
||||
gl.BufferData(opengl.ARRAY_BUFFER, len(vertices)*4, vertices, opengl.STATIC_DRAW)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
}
|
||||
grid.vao.WithSetter(func(gl opengl.OpenGL) {
|
||||
gl.EnableVertexAttribArray(uint32(grid.vertexPositionAttrib))
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, grid.vertexPositionBuffer)
|
||||
gl.VertexAttribOffset(uint32(grid.vertexPositionAttrib), 3, opengl.FLOAT, false, 0, 0)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ColorDisplay is an enumeration to how a tile should be colored.
|
||||
type ColorDisplay int
|
||||
|
||||
// ColorDisplay constants are listed below.
|
||||
const (
|
||||
ColorDisplayNone ColorDisplay = 0
|
||||
ColorDisplayFloor ColorDisplay = 1
|
||||
ColorDisplayCeiling ColorDisplay = 2
|
||||
)
|
||||
|
||||
// String returns a textual representation.
|
||||
func (display ColorDisplay) String() string {
|
||||
switch display {
|
||||
case ColorDisplayNone:
|
||||
return "None"
|
||||
case ColorDisplayFloor:
|
||||
return "Floor"
|
||||
case ColorDisplayCeiling:
|
||||
return "Ceiling"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown%d", int(display))
|
||||
}
|
||||
}
|
||||
|
||||
// ColorDisplays returns all ColorDisplay constants.
|
||||
func ColorDisplays() []ColorDisplay {
|
||||
return []ColorDisplay{ColorDisplayNone, ColorDisplayFloor, ColorDisplayCeiling}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package levels
|
||||
|
||||
const (
|
||||
hintUnknown = "???"
|
||||
hintReadOnly = " (read-only)"
|
||||
)
|
||||
@@ -0,0 +1,409 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||
"github.com/inkyblackness/hacked/ss1/edit"
|
||||
"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"
|
||||
)
|
||||
|
||||
// ControlView is the core view for level editing.
|
||||
type ControlView struct {
|
||||
levels *edit.EditableLevels
|
||||
levelSelection *edit.LevelSelectionService
|
||||
editor *edit.LevelEditorService
|
||||
|
||||
guiScale float32
|
||||
registry cmd.Registry
|
||||
|
||||
textCache *text.Cache
|
||||
textureCache *graphics.TextureCache
|
||||
|
||||
model controlViewModel
|
||||
}
|
||||
|
||||
// NewControlView returns a new instance.
|
||||
func NewControlView(levels *edit.EditableLevels, levelSelection *edit.LevelSelectionService, editor *edit.LevelEditorService,
|
||||
guiScale float32, textCache *text.Cache, textureCache *graphics.TextureCache,
|
||||
registry cmd.Registry) *ControlView {
|
||||
view := &ControlView{
|
||||
levels: levels,
|
||||
levelSelection: levelSelection,
|
||||
editor: editor,
|
||||
|
||||
guiScale: guiScale,
|
||||
registry: registry,
|
||||
textCache: textCache,
|
||||
textureCache: textureCache,
|
||||
model: freshControlViewModel(),
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// WindowOpen returns the flag address, to be used with the main menu.
|
||||
func (view *ControlView) WindowOpen() *bool {
|
||||
return &view.model.windowOpen
|
||||
}
|
||||
|
||||
// Render renders the view.
|
||||
func (view *ControlView) Render() {
|
||||
if view.model.restoreFocus {
|
||||
imgui.SetNextWindowFocus()
|
||||
view.model.restoreFocus = false
|
||||
view.model.windowOpen = true
|
||||
}
|
||||
if view.model.windowOpen {
|
||||
imgui.SetNextWindowSizeV(imgui.Vec2{X: 400 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||
title := "Level Control"
|
||||
readOnly := view.editor.IsReadOnly()
|
||||
if readOnly {
|
||||
title += hintReadOnly
|
||||
}
|
||||
if imgui.BeginV(title+"###Level Control", view.WindowOpen(), imgui.WindowFlagsNoCollapse) {
|
||||
view.renderContent(view.editor.Level(), readOnly)
|
||||
}
|
||||
imgui.End()
|
||||
}
|
||||
}
|
||||
|
||||
var levelHeights = []string{
|
||||
"32 Tiles",
|
||||
"16 Tiles",
|
||||
"8 Tiles",
|
||||
"4 Tiles",
|
||||
"2 Tiles",
|
||||
"1 Tile",
|
||||
"1/2 Tile",
|
||||
"1/4 Tile",
|
||||
}
|
||||
|
||||
func (view *ControlView) renderContent(lvl *level.Level, readOnly bool) {
|
||||
imgui.PushItemWidth(-200 * view.guiScale)
|
||||
selectedLevel := view.levelSelection.CurrentLevelID()
|
||||
if gui.StepSliderInt("Active Level", &selectedLevel, 0, archive.MaxLevels-1) {
|
||||
view.levelSelection.SetCurrentLevelID(selectedLevel)
|
||||
}
|
||||
imgui.Separator()
|
||||
levelType := "Real World"
|
||||
if lvl.IsCyberspace() {
|
||||
levelType = "Cyberspace"
|
||||
}
|
||||
imgui.LabelText("Type", levelType)
|
||||
view.renderLevelHeight(lvl, readOnly)
|
||||
|
||||
if !lvl.IsCyberspace() {
|
||||
view.renderTextureAtlas(lvl, readOnly)
|
||||
view.renderSurveillanceObjects(lvl, readOnly)
|
||||
view.renderHazards(lvl, readOnly)
|
||||
view.renderTextureAnimations(lvl, readOnly)
|
||||
}
|
||||
|
||||
imgui.PopItemWidth()
|
||||
}
|
||||
|
||||
func (view *ControlView) renderLevelHeight(lvl *level.Level, readOnly bool) {
|
||||
_, _, currentShift := lvl.Size()
|
||||
if readOnly {
|
||||
imgui.LabelText("Level Height", levelHeights[currentShift])
|
||||
} else if imgui.BeginCombo("Level Height", levelHeights[currentShift]) {
|
||||
for shift, height := range levelHeights {
|
||||
if imgui.SelectableV(height, shift == int(currentShift), 0, imgui.Vec2{}) {
|
||||
view.requestSetZShift(lvl, shift)
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *ControlView) textureName(index int) string {
|
||||
key := resource.KeyOf(ids.TextureNames, resource.LangDefault, index)
|
||||
name, err := view.textCache.Text(key)
|
||||
suffix := ""
|
||||
if err == nil {
|
||||
suffix = ": " + name
|
||||
}
|
||||
return fmt.Sprintf("%3d", index) + suffix
|
||||
}
|
||||
|
||||
func (view *ControlView) renderTextureAtlas(lvl *level.Level, readOnly bool) {
|
||||
imgui.Separator()
|
||||
|
||||
atlas := lvl.TextureAtlas()
|
||||
{
|
||||
render.TextureSelector("Level Textures", -200, view.guiScale,
|
||||
len(atlas), view.model.selectedAtlasIndex,
|
||||
view.textureCache,
|
||||
func(index int) resource.Key {
|
||||
return resource.KeyOf(ids.LargeTextures.Plus(int(atlas[index])), resource.LangAny, 0)
|
||||
},
|
||||
func(index int) string {
|
||||
textureType := "(F/C/W)"
|
||||
if index >= level.FloorCeilingTextureLimit {
|
||||
textureType = "(walls only)"
|
||||
}
|
||||
return fmt.Sprintf("Atlas Index %2d ", index) + textureType + "\n" + view.textureName(int(atlas[index]))
|
||||
}, func(newIndex int) {
|
||||
view.model.selectedAtlasIndex = newIndex
|
||||
})
|
||||
imgui.SameLine()
|
||||
imgui.Text("Level Textures")
|
||||
}
|
||||
gameTextureIndex := -1
|
||||
if (view.model.selectedAtlasIndex >= 0) && (view.model.selectedAtlasIndex < len(atlas)) {
|
||||
gameTextureIndex = int(atlas[view.model.selectedAtlasIndex])
|
||||
}
|
||||
if !readOnly {
|
||||
render.TextureSelector("Game Textures", -200, view.guiScale,
|
||||
world.MaxWorldTextures, gameTextureIndex,
|
||||
view.textureCache,
|
||||
func(index int) resource.Key {
|
||||
return resource.KeyOf(ids.LargeTextures.Plus(index), resource.LangAny, 0)
|
||||
},
|
||||
view.textureName,
|
||||
func(newIndex int) {
|
||||
view.requestSetLevelTexture(lvl, view.model.selectedAtlasIndex, newIndex)
|
||||
})
|
||||
imgui.SameLine()
|
||||
imgui.Text("Game Textures")
|
||||
}
|
||||
}
|
||||
|
||||
func (view *ControlView) renderSurveillanceObjects(lvl *level.Level, readOnly bool) {
|
||||
imgui.Separator()
|
||||
|
||||
if imgui.BeginCombo("Surveillance Object", fmt.Sprintf("Object %d", view.model.selectedSurveillanceObjectIndex)) {
|
||||
for i := 0; i < level.SurveillanceObjectCount; i++ {
|
||||
if imgui.SelectableV(fmt.Sprintf("Object %d", i), i == view.model.selectedSurveillanceObjectIndex, 0, imgui.Vec2{}) {
|
||||
view.model.selectedSurveillanceObjectIndex = i
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
sources := lvl.SurveillanceSources()
|
||||
surrogates := lvl.SurveillanceSurrogates()
|
||||
capacity := lvl.ObjectCapacity()
|
||||
|
||||
view.renderSliderInt(readOnly, "Surveillance Source", int(sources[view.model.selectedSurveillanceObjectIndex]),
|
||||
func(int) string { return "%d" },
|
||||
0, capacity,
|
||||
func(newValue int) {
|
||||
view.requestSetSurveillanceSource(lvl, view.model.selectedSurveillanceObjectIndex, level.ObjectID(newValue))
|
||||
})
|
||||
view.renderSliderInt(readOnly, "Surveillance Surrogate", int(surrogates[view.model.selectedSurveillanceObjectIndex]),
|
||||
func(int) string { return "%d" },
|
||||
0, capacity,
|
||||
func(newValue int) {
|
||||
view.requestSetSurveillanceSurrogate(lvl, view.model.selectedSurveillanceObjectIndex, level.ObjectID(newValue))
|
||||
})
|
||||
}
|
||||
|
||||
func (view *ControlView) renderHazards(lvl *level.Level, readOnly bool) {
|
||||
imgui.Separator()
|
||||
|
||||
parameters := lvl.Parameters()
|
||||
currentCeiling := currentCeilingHazard(parameters)
|
||||
if readOnly {
|
||||
imgui.LabelText("Ceiling Hazard", currentCeiling.title)
|
||||
} else if imgui.BeginCombo("Ceiling Hazard", currentCeiling.title) {
|
||||
for _, info := range ceilingHazards {
|
||||
if imgui.SelectableV(info.title, info.title == currentCeiling.title, 0, imgui.Vec2{}) {
|
||||
view.requestSetCeilingHazard(lvl, info)
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
view.renderSliderInt(readOnly, "Ceiling Hazard Level", int(parameters.CeilingHazardLevel),
|
||||
currentCeiling.formatter, 0, 255,
|
||||
func(newValue int) {
|
||||
view.requestSetCeilingHazardLevel(lvl, byte(newValue))
|
||||
})
|
||||
|
||||
currentFloor := currentFloorHazard(parameters)
|
||||
if readOnly {
|
||||
imgui.LabelText("Floor Hazard", currentFloor.title)
|
||||
} else if imgui.BeginCombo("Floor Hazard", currentFloor.title) {
|
||||
for _, info := range floorHazards {
|
||||
if imgui.SelectableV(info.title, info.title == currentFloor.title, 0, imgui.Vec2{}) {
|
||||
view.requestSetFloorHazard(lvl, info)
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
view.renderSliderInt(readOnly, "Floor Hazard Level", int(parameters.FloorHazardLevel),
|
||||
currentFloor.formatter, 0, 255,
|
||||
func(newValue int) {
|
||||
view.requestSetFloorHazardLevel(lvl, byte(newValue))
|
||||
})
|
||||
}
|
||||
|
||||
func (view *ControlView) renderTextureAnimations(lvl *level.Level, readOnly bool) {
|
||||
imgui.Separator()
|
||||
|
||||
animations := lvl.TextureAnimations()
|
||||
selectedText := ""
|
||||
if (view.model.selectedTextureAnimationIndex >= 1) && (view.model.selectedTextureAnimationIndex < len(animations)) {
|
||||
selectedText = fmt.Sprintf("%d", view.model.selectedTextureAnimationIndex)
|
||||
}
|
||||
if imgui.BeginCombo("Texture Animation Group", selectedText) {
|
||||
for i := 1; i < len(animations); i++ {
|
||||
if imgui.SelectableV(fmt.Sprintf("%2d", i), view.model.selectedTextureAnimationIndex == i, 0, imgui.Vec2{}) {
|
||||
view.model.selectedTextureAnimationIndex = i
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
|
||||
if (view.model.selectedTextureAnimationIndex >= 1) && (view.model.selectedTextureAnimationIndex < len(animations)) {
|
||||
animation := animations[view.model.selectedTextureAnimationIndex]
|
||||
view.renderSliderInt(readOnly, "Texture Animation Time", int(animation.FrameTime),
|
||||
func(int) string { return "%d msec" },
|
||||
0, 1000,
|
||||
func(newValue int) {
|
||||
view.requestSetTextureAnimationTime(lvl, view.model.selectedTextureAnimationIndex, uint16(newValue))
|
||||
})
|
||||
view.renderSliderInt(readOnly, "Texture Animation Frame Count", int(animation.FrameCount),
|
||||
func(int) string { return "%d" },
|
||||
0, 10,
|
||||
func(newValue int) {
|
||||
view.requestSetTextureAnimationFrameCount(lvl, view.model.selectedTextureAnimationIndex, byte(newValue))
|
||||
})
|
||||
if readOnly {
|
||||
imgui.LabelText("Texture Animation Loop Type", animation.LoopType.String())
|
||||
} else if imgui.BeginCombo("Texture Animation Loop Type", animation.LoopType.String()) {
|
||||
loopTypes := level.TextureAnimationLoopTypes()
|
||||
for _, loopType := range loopTypes {
|
||||
if imgui.SelectableV(loopType.String(), loopType == animation.LoopType, 0, imgui.Vec2{}) {
|
||||
view.requestSetTextureAnimationType(lvl, view.model.selectedTextureAnimationIndex, loopType)
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (view *ControlView) renderSliderInt(readOnly bool, label string, selectedValue int,
|
||||
formatter func(int) string, min, max int, changeHandler func(int)) {
|
||||
selectedString := formatter(selectedValue)
|
||||
labelValue := fmt.Sprintf(selectedString, selectedValue)
|
||||
if readOnly {
|
||||
imgui.LabelText(label, labelValue)
|
||||
} else if gui.StepSliderIntV(label, &selectedValue, min, max, selectedString) {
|
||||
changeHandler(selectedValue)
|
||||
}
|
||||
}
|
||||
|
||||
func (view *ControlView) requestSetZShift(lvl *level.Level, newValue int) {
|
||||
lvl.SetHeightShift(level.HeightShift(newValue))
|
||||
view.patchLevelResources(lvl, func() {})
|
||||
}
|
||||
|
||||
func (view *ControlView) requestSetLevelTexture(lvl *level.Level, atlasIndex, worldTextureIndex int) {
|
||||
lvl.SetTextureAtlasEntry(atlasIndex, level.TextureIndex(worldTextureIndex))
|
||||
view.patchLevelResources(lvl, func() {
|
||||
view.model.selectedAtlasIndex = atlasIndex
|
||||
})
|
||||
}
|
||||
|
||||
func (view *ControlView) requestSetSurveillanceSource(lvl *level.Level, objectIndex int, objectID level.ObjectID) {
|
||||
lvl.SetSurveillanceSource(objectIndex, objectID)
|
||||
view.patchLevelResources(lvl, func() {
|
||||
view.model.selectedSurveillanceObjectIndex = objectIndex
|
||||
})
|
||||
}
|
||||
|
||||
func (view *ControlView) requestSetSurveillanceSurrogate(lvl *level.Level, objectIndex int, objectID level.ObjectID) {
|
||||
lvl.SetSurveillanceSurrogate(objectIndex, objectID)
|
||||
view.patchLevelResources(lvl, func() {
|
||||
view.model.selectedSurveillanceObjectIndex = objectIndex
|
||||
})
|
||||
}
|
||||
|
||||
func (view *ControlView) requestSetCeilingHazard(lvl *level.Level, info ceilingHazardInfo) {
|
||||
parameters := lvl.Parameters()
|
||||
parameters.RadiationRegister = 0
|
||||
if info.radiationRegister {
|
||||
parameters.RadiationRegister = 2
|
||||
}
|
||||
view.patchLevelResources(lvl, func() {})
|
||||
}
|
||||
|
||||
func (view *ControlView) requestSetCeilingHazardLevel(lvl *level.Level, value byte) {
|
||||
lvl.Parameters().CeilingHazardLevel = value
|
||||
view.patchLevelResources(lvl, func() {})
|
||||
}
|
||||
|
||||
func (view *ControlView) requestSetFloorHazard(lvl *level.Level, info floorHazardInfo) {
|
||||
parameters := lvl.Parameters()
|
||||
parameters.BiohazardRegister = 0
|
||||
parameters.FloorHazardIsGravity = 0
|
||||
if info.isGravity {
|
||||
parameters.FloorHazardIsGravity = 1
|
||||
} else if info.biohazardRegister {
|
||||
parameters.BiohazardRegister = 2
|
||||
}
|
||||
view.patchLevelResources(lvl, func() {})
|
||||
}
|
||||
|
||||
func (view *ControlView) requestSetFloorHazardLevel(lvl *level.Level, value byte) {
|
||||
lvl.Parameters().FloorHazardLevel = value
|
||||
view.patchLevelResources(lvl, func() {})
|
||||
}
|
||||
|
||||
func (view *ControlView) requestSetTextureAnimationTime(lvl *level.Level, index int, value uint16) {
|
||||
lvl.TextureAnimations()[index].FrameTime = value
|
||||
view.patchLevelResources(lvl, func() {
|
||||
view.model.selectedTextureAnimationIndex = index
|
||||
})
|
||||
}
|
||||
|
||||
func (view *ControlView) requestSetTextureAnimationFrameCount(lvl *level.Level, index int, value byte) {
|
||||
lvl.TextureAnimations()[index].FrameCount = value
|
||||
view.patchLevelResources(lvl, func() {
|
||||
view.model.selectedTextureAnimationIndex = index
|
||||
})
|
||||
}
|
||||
|
||||
func (view *ControlView) requestSetTextureAnimationType(lvl *level.Level, index int, value level.TextureAnimationLoopType) {
|
||||
lvl.TextureAnimations()[index].LoopType = value
|
||||
view.patchLevelResources(lvl, func() {
|
||||
view.model.selectedTextureAnimationIndex = index
|
||||
})
|
||||
}
|
||||
|
||||
func (view *ControlView) patchLevelResources(lvl *level.Level, extraRestoreState func()) {
|
||||
oldLevelID := view.levelSelection.CurrentLevelID()
|
||||
extraTask := func(world.Modder) error {
|
||||
extraRestoreState()
|
||||
return nil
|
||||
}
|
||||
|
||||
err := view.registry.Register(cmd.Named("PatchLevel"),
|
||||
cmd.Forward(view.restoreFocusTask()),
|
||||
cmd.Forward(view.levelSelection.SetCurrentLevelIDTask(lvl.ID())),
|
||||
cmd.Forward(extraTask),
|
||||
cmd.Nested(func() error { return view.levels.CommitLevelChanges(lvl.ID()) }),
|
||||
cmd.Reverse(extraTask),
|
||||
cmd.Reverse(view.levelSelection.SetCurrentLevelIDTask(oldLevelID)),
|
||||
cmd.Reverse(view.restoreFocusTask()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (view *ControlView) restoreFocusTask() cmd.Task {
|
||||
return func(modder world.Modder) error {
|
||||
view.model.restoreFocus = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package levels
|
||||
|
||||
type controlViewModel struct {
|
||||
selectedAtlasIndex int
|
||||
selectedSurveillanceObjectIndex int
|
||||
selectedTextureAnimationIndex int
|
||||
|
||||
restoreFocus bool
|
||||
windowOpen bool
|
||||
}
|
||||
|
||||
func freshControlViewModel() controlViewModel {
|
||||
return controlViewModel{
|
||||
selectedTextureAnimationIndex: 1,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
)
|
||||
|
||||
func tileHeightFormatterFor(levelHeight level.HeightShift) func(int) string {
|
||||
return func(value int) string {
|
||||
tileHeight, err := levelHeight.ValueFromTileHeight(level.TileHeightUnit(value))
|
||||
tileHeightString := hintUnknown
|
||||
if err == nil {
|
||||
tileHeightString = fmt.Sprintf("%2.3f", tileHeight)
|
||||
}
|
||||
return tileHeightString + " tile(s) - raw: %d"
|
||||
}
|
||||
}
|
||||
|
||||
func objectHeightFormatterFor(levelHeight level.HeightShift) func(int) string {
|
||||
return func(value int) string {
|
||||
tileHeight, err := levelHeight.ValueFromObjectHeight(level.HeightUnit(value))
|
||||
tileHeightString := hintUnknown
|
||||
if err == nil {
|
||||
tileHeightString = fmt.Sprintf("%2.3f", tileHeight)
|
||||
}
|
||||
return tileHeightString + " tile(s) - raw: %d"
|
||||
}
|
||||
}
|
||||
|
||||
func moveTileHeightFormatterFor(levelHeight level.HeightShift) func(int) string {
|
||||
normalFormatter := tileHeightFormatterFor(levelHeight)
|
||||
return func(value int) string {
|
||||
if (value >= 0) && (value <= int(level.TileHeightUnitMax)) {
|
||||
return normalFormatter(value)
|
||||
}
|
||||
return "Don't change - raw: %d"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
mgl "github.com/go-gl/mathgl/mgl32"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
)
|
||||
|
||||
var highlighterVertexShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
in vec3 vertexPosition;
|
||||
|
||||
uniform mat4 modelMatrix;
|
||||
uniform mat4 viewMatrix;
|
||||
uniform mat4 projectionMatrix;
|
||||
|
||||
void main(void) {
|
||||
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
|
||||
}
|
||||
`
|
||||
|
||||
var highlighterFragmentShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
uniform vec4 inColor;
|
||||
out vec4 fragColor;
|
||||
|
||||
void main(void) {
|
||||
fragColor = inColor;
|
||||
}
|
||||
`
|
||||
|
||||
// Highlighter draws a simple highlighting of a rectangular area.
|
||||
type Highlighter struct {
|
||||
context *render.Context
|
||||
|
||||
program uint32
|
||||
vao *opengl.VertexArrayObject
|
||||
vertexPositionBuffer uint32
|
||||
vertexPositionAttrib int32
|
||||
modelMatrixUniform opengl.Matrix4Uniform
|
||||
viewMatrixUniform opengl.Matrix4Uniform
|
||||
projectionMatrixUniform opengl.Matrix4Uniform
|
||||
inColorUniform opengl.Vector4Uniform
|
||||
}
|
||||
|
||||
// NewHighlighter returns a new instance of Highlighter.
|
||||
func NewHighlighter(context *render.Context) *Highlighter {
|
||||
gl := context.OpenGL
|
||||
program, programErr := opengl.LinkNewStandardProgram(gl, highlighterVertexShaderSource, highlighterFragmentShaderSource)
|
||||
|
||||
if programErr != nil {
|
||||
panic(opengl.NamedShaderError{Name: "HighlighterShader", Nested: programErr})
|
||||
}
|
||||
highlighter := &Highlighter{
|
||||
context: context,
|
||||
program: program,
|
||||
|
||||
vao: opengl.NewVertexArrayObject(gl, program),
|
||||
vertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||
vertexPositionAttrib: gl.GetAttribLocation(program, "vertexPosition"),
|
||||
modelMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "modelMatrix")),
|
||||
viewMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "viewMatrix")),
|
||||
projectionMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "projectionMatrix")),
|
||||
inColorUniform: opengl.Vector4Uniform(gl.GetUniformLocation(program, "inColor"))}
|
||||
|
||||
{
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, highlighter.vertexPositionBuffer)
|
||||
half := float32(0.5)
|
||||
var vertices = []float32{
|
||||
-half, -half, 0.0,
|
||||
half, -half, 0.0,
|
||||
half, half, 0.0,
|
||||
|
||||
half, half, 0.0,
|
||||
-half, half, 0.0,
|
||||
-half, -half, 0.0}
|
||||
gl.BufferData(opengl.ARRAY_BUFFER, len(vertices)*4, vertices, opengl.STATIC_DRAW)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
}
|
||||
|
||||
highlighter.vao.OnShader(func() {
|
||||
gl.EnableVertexAttribArray(uint32(highlighter.vertexPositionAttrib))
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, highlighter.vertexPositionBuffer)
|
||||
gl.VertexAttribOffset(uint32(highlighter.vertexPositionAttrib), 3, opengl.FLOAT, false, 0, 0)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
})
|
||||
|
||||
return highlighter
|
||||
}
|
||||
|
||||
// Dispose releases all resources.
|
||||
func (highlighter *Highlighter) Dispose() {
|
||||
gl := highlighter.context.OpenGL
|
||||
|
||||
highlighter.vao.Dispose()
|
||||
gl.DeleteBuffers([]uint32{highlighter.vertexPositionBuffer})
|
||||
gl.DeleteShader(highlighter.program)
|
||||
}
|
||||
|
||||
// Render renders the highlights.
|
||||
func (highlighter *Highlighter) Render(positions []MapPosition, sideLength float32, color [4]float32) {
|
||||
gl := highlighter.context.OpenGL
|
||||
|
||||
highlighter.vao.OnShader(func() {
|
||||
highlighter.viewMatrixUniform.Set(gl, highlighter.context.ViewMatrix)
|
||||
highlighter.projectionMatrixUniform.Set(gl, &highlighter.context.ProjectionMatrix)
|
||||
highlighter.inColorUniform.Set(gl, &color)
|
||||
|
||||
for _, pos := range positions {
|
||||
modelMatrix := mgl.Ident4().
|
||||
Mul4(mgl.Translate3D(float32(pos.X), float32(pos.Y), 0.0)).
|
||||
Mul4(mgl.Scale3D(sideLength, sideLength, 1.0))
|
||||
|
||||
highlighter.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||
|
||||
gl.DrawArrays(opengl.TRIANGLES, 0, 6)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
mgl "github.com/go-gl/mathgl/mgl32"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
)
|
||||
|
||||
type hoverItem interface {
|
||||
Pos() MapPosition
|
||||
Size() float32
|
||||
IsIn(lvl *level.Level) bool
|
||||
}
|
||||
|
||||
type tileHoverItem struct {
|
||||
pos MapPosition
|
||||
}
|
||||
|
||||
func (item tileHoverItem) Pos() MapPosition {
|
||||
return item.pos
|
||||
}
|
||||
|
||||
func (item tileHoverItem) Size() float32 {
|
||||
return level.FineCoordinatesPerTileSide
|
||||
}
|
||||
|
||||
func (item tileHoverItem) IsIn(lvl *level.Level) bool {
|
||||
return lvl.Tile(item.pos.Tile()) != nil
|
||||
}
|
||||
|
||||
type objectHoverItem struct {
|
||||
id level.ObjectID
|
||||
pos MapPosition
|
||||
}
|
||||
|
||||
func (item objectHoverItem) Pos() MapPosition {
|
||||
return item.pos
|
||||
}
|
||||
|
||||
func (item objectHoverItem) Size() float32 {
|
||||
return level.FineCoordinatesPerTileSide / 4
|
||||
}
|
||||
|
||||
func (item objectHoverItem) IsIn(lvl *level.Level) bool {
|
||||
obj := lvl.Object(item.id)
|
||||
return (obj != nil) && (obj.InUse != 0) && (item.pos.X == obj.X) && (item.pos.Y == obj.Y)
|
||||
}
|
||||
|
||||
type hoverItems struct {
|
||||
available []hoverItem
|
||||
activeIndex int
|
||||
activeItem hoverItem
|
||||
}
|
||||
|
||||
func (items *hoverItems) reset() {
|
||||
items.available = nil
|
||||
items.activeIndex = 0
|
||||
items.activeItem = nil
|
||||
}
|
||||
|
||||
func (items *hoverItems) scroll(forward bool) {
|
||||
count := len(items.available)
|
||||
if count > 0 {
|
||||
diff := 1
|
||||
if !forward {
|
||||
diff = -1
|
||||
}
|
||||
items.activeIndex = (count + (items.activeIndex + diff)) % count
|
||||
items.activeItem = items.available[items.activeIndex]
|
||||
}
|
||||
}
|
||||
|
||||
func (items *hoverItems) find(lvl *level.Level, pos MapPosition) {
|
||||
refVec := mgl.Vec2{float32(pos.X), float32(pos.Y)}
|
||||
var distances []float32
|
||||
|
||||
items.available = nil
|
||||
lvl.ForEachObject(func(id level.ObjectID, entry level.ObjectMainEntry) {
|
||||
entryVec := mgl.Vec2{float32(entry.X), float32(entry.Y)}
|
||||
distance := refVec.Sub(entryVec).Len()
|
||||
if distance < level.FineCoordinatesPerTileSide/4 {
|
||||
items.available = append(items.available,
|
||||
objectHoverItem{
|
||||
id: id,
|
||||
pos: MapPosition{X: entry.X, Y: entry.Y},
|
||||
})
|
||||
distances = append(distances, distance)
|
||||
}
|
||||
})
|
||||
items.available = append(items.available,
|
||||
tileHoverItem{
|
||||
pos: MapPosition{
|
||||
X: level.CoordinateAt(pos.X.Tile(), level.FineCoordinatesPerTileSide/2),
|
||||
Y: level.CoordinateAt(pos.Y.Tile(), level.FineCoordinatesPerTileSide/2),
|
||||
},
|
||||
})
|
||||
distances = append(distances, level.FineCoordinatesPerTileSide)
|
||||
sort.Slice(items.available, func(a, b int) bool { return distances[a] < distances[b] })
|
||||
|
||||
items.activeIndex = 0
|
||||
items.activeItem = items.available[items.activeIndex]
|
||||
}
|
||||
|
||||
func (items *hoverItems) validate(lvl *level.Level) {
|
||||
allValid := true
|
||||
for _, item := range items.available {
|
||||
if !item.IsIn(lvl) {
|
||||
allValid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allValid {
|
||||
return
|
||||
}
|
||||
|
||||
newItems := make([]hoverItem, 0, len(items.available))
|
||||
for index, item := range items.available {
|
||||
isActive := index == items.activeIndex
|
||||
if item.IsIn(lvl) {
|
||||
if isActive {
|
||||
items.activeIndex = len(newItems)
|
||||
}
|
||||
newItems = append(newItems, item)
|
||||
} else if isActive {
|
||||
items.activeIndex = 0
|
||||
}
|
||||
}
|
||||
items.available = newItems
|
||||
items.activeItem = nil
|
||||
if items.activeIndex < len(items.available) {
|
||||
items.activeItem = items.available[items.activeIndex]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
mgl "github.com/go-gl/mathgl/mgl32"
|
||||
)
|
||||
|
||||
// LimitedCamera is a camera implementation with ranged controls for zooming and moving.
|
||||
type LimitedCamera struct {
|
||||
viewportWidth, viewportHeight float32
|
||||
minZoom, maxZoom float32
|
||||
minPos, maxPos float32
|
||||
|
||||
requestedZoomLevel float32
|
||||
viewOffsetX, viewOffsetY float32
|
||||
|
||||
viewMatrix mgl.Mat4
|
||||
}
|
||||
|
||||
// NewLimitedCamera returns a new instance of a LimitedCamera.
|
||||
func NewLimitedCamera(minZoom, maxZoom float32, minPos, maxPos float32) *LimitedCamera {
|
||||
cam := &LimitedCamera{
|
||||
viewportWidth: 1.0,
|
||||
viewportHeight: 1.0,
|
||||
minZoom: minZoom,
|
||||
maxZoom: maxZoom,
|
||||
minPos: minPos,
|
||||
maxPos: maxPos,
|
||||
viewMatrix: mgl.Ident4()}
|
||||
|
||||
return cam
|
||||
}
|
||||
|
||||
// SetViewportSize notifies the camera how big the view is.
|
||||
func (cam *LimitedCamera) SetViewportSize(width, height float32) {
|
||||
if (cam.viewportWidth != width) || (cam.viewportHeight != height) {
|
||||
cam.viewportWidth, cam.viewportHeight = width, height
|
||||
cam.updateViewMatrix()
|
||||
}
|
||||
}
|
||||
|
||||
// ViewMatrix implements the Viewer interface.
|
||||
func (cam *LimitedCamera) ViewMatrix() *mgl.Mat4 {
|
||||
return &cam.viewMatrix
|
||||
}
|
||||
|
||||
// MoveBy adjusts the requested view offset by given delta values in world coordinates.
|
||||
func (cam *LimitedCamera) MoveBy(dx, dy float32) {
|
||||
cam.MoveTo(cam.viewOffsetX+dx, cam.viewOffsetY+dy)
|
||||
}
|
||||
|
||||
// MoveTo sets the requested view offset to the given world coordinates.
|
||||
func (cam *LimitedCamera) MoveTo(worldX, worldY float32) {
|
||||
cam.viewOffsetX = cam.limitValue(worldX, -cam.maxPos, cam.minPos)
|
||||
cam.viewOffsetY = cam.limitValue(worldY, -cam.maxPos, cam.minPos)
|
||||
cam.updateViewMatrix()
|
||||
}
|
||||
|
||||
// ZoomAt adjusts the requested zoom level by given delta, centered around given world position.
|
||||
// Positive values zoom in.
|
||||
func (cam *LimitedCamera) ZoomAt(levelDelta float32, x, y float32) {
|
||||
cam.requestedZoomLevel = cam.limitValue(cam.requestedZoomLevel+levelDelta, cam.minZoom, cam.maxZoom)
|
||||
|
||||
focusPoint := mgl.Vec4{x, y, 0.0, 1.0}
|
||||
oldPixel := cam.viewMatrix.Mul4x1(focusPoint)
|
||||
|
||||
cam.updateViewMatrix()
|
||||
|
||||
newPixel := cam.viewMatrix.Mul4x1(focusPoint)
|
||||
scaleFactor := cam.scaleFactor()
|
||||
cam.MoveBy(-(newPixel[0]-oldPixel[0])/scaleFactor, +(newPixel[1]-oldPixel[1])/scaleFactor)
|
||||
}
|
||||
|
||||
func (cam *LimitedCamera) limitValue(value float32, min, max float32) float32 {
|
||||
result := value
|
||||
|
||||
if result < min {
|
||||
result = min
|
||||
}
|
||||
if result > max {
|
||||
result = max
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (cam *LimitedCamera) scaleFactor() float32 {
|
||||
return float32(math.Pow(2.0, float64(cam.requestedZoomLevel)))
|
||||
}
|
||||
|
||||
func (cam *LimitedCamera) updateViewMatrix() {
|
||||
scaleFactor := cam.scaleFactor()
|
||||
cam.viewMatrix = mgl.Ident4().
|
||||
Mul4(mgl.Translate3D(cam.viewportWidth/2.0, cam.viewportHeight/2.0, 0)).
|
||||
Mul4(mgl.Scale3D(scaleFactor, -scaleFactor, 1.0)).
|
||||
Mul4(mgl.Translate3D(cam.viewOffsetX, cam.viewOffsetY, 0))
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
mgl "github.com/go-gl/mathgl/mgl32"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
)
|
||||
|
||||
var mapColorsVertexShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
in vec4 vertexColor;
|
||||
in vec2 vertexPosition;
|
||||
|
||||
uniform mat4 modelMatrix;
|
||||
uniform mat4 viewMatrix;
|
||||
uniform mat4 projectionMatrix;
|
||||
|
||||
out vec4 color;
|
||||
|
||||
void main(void) {
|
||||
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition.xy, 0.0, 1.0);
|
||||
color = vertexColor;
|
||||
}
|
||||
`
|
||||
|
||||
var mapColorsFragmentShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
in vec4 color;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
void main(void) {
|
||||
fragColor = color;
|
||||
}
|
||||
`
|
||||
|
||||
// ColorQuery returns the color for the specified tile.
|
||||
type ColorQuery func(level.TilePosition) [4]float32
|
||||
|
||||
// MapColors is a renderable for the tile colorings.
|
||||
type MapColors struct {
|
||||
context *render.Context
|
||||
|
||||
program uint32
|
||||
vao *opengl.VertexArrayObject
|
||||
vertexPositionBuffer uint32
|
||||
vertexPositionAttrib int32
|
||||
vertexColorBuffer uint32
|
||||
vertexColorAttrib int32
|
||||
modelMatrixUniform opengl.Matrix4Uniform
|
||||
viewMatrixUniform opengl.Matrix4Uniform
|
||||
projectionMatrixUniform opengl.Matrix4Uniform
|
||||
|
||||
lastColorGrid [][][4]float32
|
||||
lastColorGridColumns int
|
||||
lastColorGridRows int
|
||||
}
|
||||
|
||||
// NewMapColors returns a new instance of a renderable for tile colorings.
|
||||
func NewMapColors(context *render.Context) *MapColors {
|
||||
gl := context.OpenGL
|
||||
program, programErr := opengl.LinkNewStandardProgram(gl, mapColorsVertexShaderSource, mapColorsFragmentShaderSource)
|
||||
|
||||
if programErr != nil {
|
||||
panic(opengl.NamedShaderError{Name: "MapColorsShader", Nested: programErr})
|
||||
}
|
||||
renderable := &MapColors{
|
||||
context: context,
|
||||
program: program,
|
||||
vao: opengl.NewVertexArrayObject(gl, program),
|
||||
vertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||
vertexPositionAttrib: gl.GetAttribLocation(program, "vertexPosition"),
|
||||
vertexColorBuffer: gl.GenBuffers(1)[0],
|
||||
vertexColorAttrib: gl.GetAttribLocation(program, "vertexColor"),
|
||||
modelMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "modelMatrix")),
|
||||
viewMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "viewMatrix")),
|
||||
projectionMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "projectionMatrix")),
|
||||
}
|
||||
|
||||
{
|
||||
top := float32(level.FineCoordinatesPerTileSide)
|
||||
left := float32(0.0)
|
||||
right := float32(level.FineCoordinatesPerTileSide)
|
||||
bottom := float32(0.0)
|
||||
|
||||
vertices := []float32{
|
||||
left, bottom,
|
||||
left, top,
|
||||
right, top,
|
||||
|
||||
right, top,
|
||||
right, bottom,
|
||||
left, bottom}
|
||||
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexPositionBuffer)
|
||||
gl.BufferData(opengl.ARRAY_BUFFER, len(vertices)*4, vertices, opengl.STATIC_DRAW)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
}
|
||||
|
||||
renderable.vao.WithSetter(func(gl opengl.OpenGL) {
|
||||
gl.EnableVertexAttribArray(uint32(renderable.vertexPositionAttrib))
|
||||
gl.EnableVertexAttribArray(uint32(renderable.vertexColorAttrib))
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexPositionBuffer)
|
||||
gl.VertexAttribOffset(uint32(renderable.vertexPositionAttrib), 2, opengl.FLOAT, false, 0, 0)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexColorBuffer)
|
||||
gl.VertexAttribOffset(uint32(renderable.vertexColorAttrib), 4, opengl.FLOAT, false, 0, 0)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
})
|
||||
|
||||
return renderable
|
||||
}
|
||||
|
||||
// Dispose releases any internal resources.
|
||||
func (renderable *MapColors) Dispose() {
|
||||
gl := renderable.context.OpenGL
|
||||
gl.DeleteProgram(renderable.program)
|
||||
gl.DeleteBuffers([]uint32{renderable.vertexPositionBuffer})
|
||||
renderable.vao.Dispose()
|
||||
}
|
||||
|
||||
// Render renders the renderable.
|
||||
func (renderable *MapColors) Render(columnCount, rowCount int, query ColorQuery) {
|
||||
gl := renderable.context.OpenGL
|
||||
|
||||
if (renderable.lastColorGridColumns != columnCount) || (renderable.lastColorGridRows != rowCount) {
|
||||
renderable.lastColorGrid = make([][][4]float32, rowCount+1)
|
||||
for y := 0; y < rowCount; y++ {
|
||||
renderable.lastColorGrid[y] = make([][4]float32, columnCount+1)
|
||||
}
|
||||
renderable.lastColorGrid[rowCount] = make([][4]float32, columnCount+1)
|
||||
renderable.lastColorGridColumns = columnCount
|
||||
renderable.lastColorGridRows = rowCount
|
||||
}
|
||||
for y, row := range renderable.lastColorGrid {
|
||||
for x := 0; x < columnCount; x++ {
|
||||
row[x] = query(level.TilePosition{X: byte(x), Y: byte(y)})
|
||||
}
|
||||
}
|
||||
|
||||
renderable.vao.OnShader(func() {
|
||||
var colors [24]float32
|
||||
|
||||
renderable.viewMatrixUniform.Set(gl, renderable.context.ViewMatrix)
|
||||
renderable.projectionMatrixUniform.Set(gl, &renderable.context.ProjectionMatrix)
|
||||
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexColorBuffer)
|
||||
|
||||
for y := 0; y < rowCount; y++ {
|
||||
for x := 0; x < columnCount; x++ {
|
||||
colorBottomLeft := renderable.lastColorGrid[y][x]
|
||||
colorBottomRight := renderable.lastColorGrid[y][x+1]
|
||||
colorTopLeft := renderable.lastColorGrid[y+1][x]
|
||||
colorTopRight := renderable.lastColorGrid[y+1][x+1]
|
||||
|
||||
modelMatrix := mgl.Ident4().
|
||||
Mul4(mgl.Translate3D((float32(x))*level.FineCoordinatesPerTileSide, (float32(y))*level.FineCoordinatesPerTileSide, 0.0))
|
||||
renderable.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||
|
||||
copy(colors[0:4], colorBottomLeft[:])
|
||||
copy(colors[4:8], colorTopLeft[:])
|
||||
copy(colors[8:12], colorTopRight[:])
|
||||
copy(colors[12:16], colorTopRight[:])
|
||||
copy(colors[16:20], colorBottomRight[:])
|
||||
copy(colors[20:24], colorBottomLeft[:])
|
||||
|
||||
gl.BufferData(opengl.ARRAY_BUFFER, len(colors)*4, colors[:], opengl.STATIC_DRAW)
|
||||
gl.DrawArrays(opengl.TRIANGLES, 0, 6)
|
||||
}
|
||||
}
|
||||
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
mgl "github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/edit"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
"github.com/inkyblackness/hacked/ui/input"
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
)
|
||||
|
||||
const contextMenuName = "MapContextMenu"
|
||||
|
||||
// MapDisplay renders a level map.
|
||||
type MapDisplay struct {
|
||||
gameObjects *edit.GameObjectsService
|
||||
levelSelection *edit.LevelSelectionService
|
||||
editor *edit.LevelEditorService
|
||||
|
||||
context render.Context
|
||||
camera *LimitedCamera
|
||||
guiScale float32
|
||||
|
||||
background *BackgroundGrid
|
||||
textures *MapTextures
|
||||
colors *MapColors
|
||||
mapGrid *MapGrid
|
||||
highlighter *Highlighter
|
||||
icons *MapIcons
|
||||
|
||||
moveCapture func(pixelX, pixelY float32)
|
||||
mouseMoved bool
|
||||
|
||||
positionPopupPos imgui.Vec2
|
||||
positionValid bool
|
||||
position MapPosition
|
||||
contextMenuRequested bool
|
||||
|
||||
selectionReference *level.TilePosition
|
||||
|
||||
hoverItems hoverItems
|
||||
}
|
||||
|
||||
// NewMapDisplay returns a new instance.
|
||||
func NewMapDisplay(gameObjects *edit.GameObjectsService, levelSelection *edit.LevelSelectionService, editor *edit.LevelEditorService,
|
||||
gl opengl.OpenGL, guiScale float32,
|
||||
textureQuery TextureQuery) *MapDisplay {
|
||||
tilesPerMapSide := float32(64)
|
||||
|
||||
tileBaseLength := float32(level.FineCoordinatesPerTileSide)
|
||||
tileBaseHalf := tileBaseLength / 2.0
|
||||
camLimit := tilesPerMapSide*tileBaseLength - tileBaseHalf
|
||||
zoomShift := guiScale - 1.0
|
||||
zoomLevelMin := float32(-5) + zoomShift
|
||||
zoomLevelMax := float32(1) + zoomShift
|
||||
|
||||
display := &MapDisplay{
|
||||
gameObjects: gameObjects,
|
||||
levelSelection: levelSelection,
|
||||
editor: editor,
|
||||
context: render.Context{
|
||||
OpenGL: gl,
|
||||
ProjectionMatrix: mgl.Ident4(),
|
||||
},
|
||||
camera: NewLimitedCamera(zoomLevelMin, zoomLevelMax, -tileBaseHalf, camLimit),
|
||||
guiScale: guiScale,
|
||||
moveCapture: func(float32, float32) {},
|
||||
}
|
||||
display.context.ViewMatrix = display.camera.ViewMatrix()
|
||||
display.background = NewBackgroundGrid(&display.context)
|
||||
display.textures = NewMapTextures(&display.context, textureQuery)
|
||||
display.colors = NewMapColors(&display.context)
|
||||
display.mapGrid = NewMapGrid(&display.context)
|
||||
display.highlighter = NewHighlighter(&display.context)
|
||||
display.icons = NewMapIcons(&display.context)
|
||||
|
||||
centerX, centerY := (tilesPerMapSide*tileBaseLength)/-2.0, (tilesPerMapSide*tileBaseLength)/-2.0
|
||||
display.camera.ZoomAt(-3+zoomShift, centerX, centerY)
|
||||
display.camera.MoveTo(centerX, centerY)
|
||||
|
||||
return display
|
||||
}
|
||||
|
||||
// Render renders the whole map display.
|
||||
func (display *MapDisplay) Render(
|
||||
paletteTexture *graphics.PaletteTexture, textureRetriever func(resource.Key) (*graphics.BitmapTexture, error),
|
||||
textureDisplay TextureDisplay, colorDisplay ColorDisplay) {
|
||||
lvl := display.editor.Level()
|
||||
columns, rows, _ := lvl.Size()
|
||||
|
||||
display.hoverItems.validate(lvl)
|
||||
|
||||
display.background.Render(columns, rows)
|
||||
if lvl.IsCyberspace() {
|
||||
display.renderCyberspaceColors(lvl, columns, rows, paletteTexture, colorDisplay)
|
||||
} else {
|
||||
display.renderRealWorldTextures(lvl, columns, rows, paletteTexture, textureDisplay)
|
||||
display.renderRealWorldShadows(lvl, columns, rows, colorDisplay)
|
||||
}
|
||||
display.mapGrid.Render(columns, rows, lvl)
|
||||
display.renderTileSelection()
|
||||
display.renderObjectBackgrounds(lvl)
|
||||
display.renderObjectIcons(lvl, paletteTexture, textureRetriever)
|
||||
display.renderObjectSelection()
|
||||
display.renderActiveHoverItem()
|
||||
display.renderPositionOverlay(lvl)
|
||||
display.renderContextMenu()
|
||||
}
|
||||
|
||||
func (display *MapDisplay) renderCyberspaceColors(lvl *level.Level, columns int, rows int, paletteTexture *graphics.PaletteTexture, colorDisplay ColorDisplay) {
|
||||
if paletteTexture == nil {
|
||||
return
|
||||
}
|
||||
var colorQuery ColorQuery
|
||||
palette := paletteTexture.Palette()
|
||||
if colorDisplay == ColorDisplayFloor {
|
||||
colorQuery = display.colorQueryFor(lvl, func(tile *level.TileMapEntry) [4]float32 {
|
||||
rgb := palette[tile.TextureInfo.FloorPaletteIndex()]
|
||||
return [4]float32{float32(rgb.Red) / 255, float32(rgb.Green) / 255, float32(rgb.Blue) / 255, 0.8}
|
||||
})
|
||||
} else if colorDisplay == ColorDisplayCeiling {
|
||||
colorQuery = display.colorQueryFor(lvl, func(tile *level.TileMapEntry) [4]float32 {
|
||||
rgb := palette[tile.TextureInfo.CeilingPaletteIndex()]
|
||||
return [4]float32{float32(rgb.Red) / 255, float32(rgb.Green) / 255, float32(rgb.Blue) / 255, 0.8}
|
||||
})
|
||||
}
|
||||
if colorQuery != nil {
|
||||
display.colors.Render(columns, rows, colorQuery)
|
||||
}
|
||||
}
|
||||
|
||||
func (display *MapDisplay) renderRealWorldTextures(lvl *level.Level, columns int, rows int,
|
||||
paletteTexture *graphics.PaletteTexture, textureDisplay TextureDisplay) {
|
||||
if paletteTexture == nil {
|
||||
return
|
||||
}
|
||||
display.textures.Render(columns, rows, func(pos level.TilePosition) (level.TileType, level.TextureIndex, int) {
|
||||
tile := lvl.Tile(pos)
|
||||
if tile == nil {
|
||||
return level.TileTypeSolid, 0, 0
|
||||
}
|
||||
atlasIndex, textureRotations := textureDisplay.Func()(tile)
|
||||
atlas := lvl.TextureAtlas()
|
||||
textureIndex := level.TextureIndex(-1)
|
||||
if (int(atlasIndex) >= 0) && (int(atlasIndex) < len(atlas)) {
|
||||
textureIndex = atlas[atlasIndex]
|
||||
}
|
||||
return tile.Type, textureIndex, textureRotations
|
||||
}, paletteTexture)
|
||||
}
|
||||
|
||||
func (display *MapDisplay) renderRealWorldShadows(lvl *level.Level, columns int, rows int, colorDisplay ColorDisplay) {
|
||||
var colorQuery ColorQuery
|
||||
if colorDisplay == ColorDisplayFloor {
|
||||
colorQuery = display.colorQueryFor(lvl, func(tile *level.TileMapEntry) [4]float32 {
|
||||
return [4]float32{0.0, 0.0, 0.0, float32(tile.Flags.ForRealWorld().FloorShadow()) / 15.0}
|
||||
})
|
||||
} else if colorDisplay == ColorDisplayCeiling {
|
||||
colorQuery = display.colorQueryFor(lvl, func(tile *level.TileMapEntry) [4]float32 {
|
||||
return [4]float32{0.0, 0.0, 0.0, float32(tile.Flags.ForRealWorld().CeilingShadow()) / 15.0}
|
||||
})
|
||||
}
|
||||
if colorQuery != nil {
|
||||
display.colors.Render(columns, rows, colorQuery)
|
||||
}
|
||||
}
|
||||
|
||||
func (display *MapDisplay) renderTileSelection() {
|
||||
selectedTiles := display.levelSelection.CurrentSelectedTiles()
|
||||
tileMapPositions := make([]MapPosition, 0, len(selectedTiles))
|
||||
for _, pos := range selectedTiles {
|
||||
tileMapPositions = append(tileMapPositions, MapPosition{
|
||||
X: level.CoordinateAt(pos.X, level.FineCoordinatesPerTileSide/2),
|
||||
Y: level.CoordinateAt(pos.Y, level.FineCoordinatesPerTileSide/2),
|
||||
})
|
||||
}
|
||||
display.highlighter.Render(tileMapPositions, level.FineCoordinatesPerTileSide, [4]float32{0.0, 0.8, 0.2, 0.5})
|
||||
}
|
||||
|
||||
func (display *MapDisplay) renderObjectBackgrounds(lvl *level.Level) {
|
||||
var objects []MapPosition
|
||||
lvl.ForEachObject(func(id level.ObjectID, entry level.ObjectMainEntry) {
|
||||
objects = append(objects, MapPosition{X: entry.X, Y: entry.Y})
|
||||
})
|
||||
display.highlighter.Render(objects, level.FineCoordinatesPerTileSide/4, [4]float32{1.0, 1.0, 1.0, 0.3})
|
||||
}
|
||||
|
||||
func (display *MapDisplay) renderObjectIcons(lvl *level.Level, paletteTexture *graphics.PaletteTexture,
|
||||
textureRetriever func(resource.Key) (*graphics.BitmapTexture, error)) {
|
||||
if paletteTexture == nil {
|
||||
return
|
||||
}
|
||||
tripleInfo := display.gameObjects.BitmapInfo()
|
||||
var icons []iconData
|
||||
var highlightIcon iconData
|
||||
var highlightID level.ObjectID
|
||||
|
||||
if display.hoverItems.activeItem != nil {
|
||||
objectItem, isObject := display.hoverItems.activeItem.(objectHoverItem)
|
||||
if isObject {
|
||||
highlightID = objectItem.id
|
||||
}
|
||||
}
|
||||
lvl.ForEachObject(func(id level.ObjectID, entry level.ObjectMainEntry) {
|
||||
triple := entry.Triple()
|
||||
info, cached := tripleInfo[triple]
|
||||
if !cached {
|
||||
return
|
||||
}
|
||||
key := resource.KeyOf(ids.ObjectBitmaps, resource.LangAny, info.Start+info.IconRecommendation)
|
||||
texture, err := textureRetriever(key)
|
||||
if err == nil {
|
||||
icon := iconData{pos: MapPosition{X: entry.X, Y: entry.Y}, texture: texture}
|
||||
if highlightID == id {
|
||||
highlightIcon = icon
|
||||
} else {
|
||||
icons = append(icons, icon)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (highlightID != 0) && (highlightIcon.texture != nil) {
|
||||
icons = append(icons, highlightIcon)
|
||||
}
|
||||
display.icons.Render(paletteTexture, level.FineCoordinatesPerTileSide/4, icons)
|
||||
}
|
||||
|
||||
func (display *MapDisplay) renderObjectSelection() {
|
||||
selectedObjects := display.editor.Objects()
|
||||
selectedObjectHighlights := make([]MapPosition, 0, len(selectedObjects))
|
||||
for _, obj := range selectedObjects {
|
||||
objPos := MapPosition{X: obj.X, Y: obj.Y}
|
||||
selectedObjectHighlights = append(selectedObjectHighlights, objPos)
|
||||
}
|
||||
display.highlighter.Render(selectedObjectHighlights, level.FineCoordinatesPerTileSide/4, [4]float32{0.0, 0.8, 0.2, 0.5})
|
||||
}
|
||||
|
||||
func (display *MapDisplay) renderActiveHoverItem() {
|
||||
if display.hoverItems.activeItem == nil {
|
||||
return
|
||||
}
|
||||
display.highlighter.Render(
|
||||
[]MapPosition{display.hoverItems.activeItem.Pos()},
|
||||
display.hoverItems.activeItem.Size(),
|
||||
[4]float32{0.0, 0.2, 0.8, 0.3})
|
||||
}
|
||||
|
||||
func (display *MapDisplay) colorQueryFor(lvl *level.Level, tileToColor func(*level.TileMapEntry) [4]float32) func(level.TilePosition) [4]float32 {
|
||||
return func(pos level.TilePosition) [4]float32 {
|
||||
tile := lvl.Tile(pos)
|
||||
if tile == nil {
|
||||
return [4]float32{}
|
||||
}
|
||||
return tileToColor(tile)
|
||||
}
|
||||
}
|
||||
|
||||
func (display *MapDisplay) renderPositionOverlay(lvl *level.Level) {
|
||||
imgui.SetNextWindowPosV(display.positionPopupPos, imgui.ConditionAlways, imgui.Vec2{X: 1.0, Y: 1.0})
|
||||
imgui.SetNextWindowSize(imgui.Vec2{X: 140 * display.guiScale, Y: 0})
|
||||
imgui.SetNextWindowBgAlpha(0.3)
|
||||
if imgui.BeginV("Position", nil, imgui.WindowFlagsNoMove|imgui.WindowFlagsNoTitleBar|imgui.WindowFlagsNoResize|imgui.WindowFlagsAlwaysAutoResize|
|
||||
imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsNoFocusOnAppearing|imgui.WindowFlagsNoNav) {
|
||||
typeString := "---"
|
||||
hasPos := false
|
||||
var pos MapPosition
|
||||
hasFloor := false
|
||||
var floorRaw int
|
||||
floorString := hintUnknown
|
||||
hasCeiling := false
|
||||
var ceilingRaw int
|
||||
ceilingString := hintUnknown
|
||||
|
||||
if display.hoverItems.activeItem != nil {
|
||||
pos = display.hoverItems.activeItem.Pos()
|
||||
hasPos = true
|
||||
|
||||
if _, isTileItem := display.hoverItems.activeItem.(tileHoverItem); isTileItem {
|
||||
pos = display.position // use raw cursor position for this display
|
||||
typeString = "Tile"
|
||||
tile := lvl.Tile(pos.Tile())
|
||||
if (tile != nil) && (tile.Type != level.TileTypeSolid) {
|
||||
_, _, heightShift := lvl.Size()
|
||||
floorHeight := tile.Floor.AbsoluteHeight()
|
||||
floorHeightInTiles, err := heightShift.ValueFromTileHeight(floorHeight)
|
||||
if err == nil {
|
||||
floorString = fmt.Sprintf("%2.3f", floorHeightInTiles)
|
||||
}
|
||||
floorRaw = int(floorHeight)
|
||||
hasFloor = true
|
||||
|
||||
ceilingHeight := tile.Ceiling.AbsoluteHeight()
|
||||
ceilingHeightInTiles, err := heightShift.ValueFromTileHeight(ceilingHeight)
|
||||
if err == nil {
|
||||
ceilingString = fmt.Sprintf("%2.3f", ceilingHeightInTiles)
|
||||
}
|
||||
ceilingRaw = int(ceilingHeight)
|
||||
hasCeiling = true
|
||||
}
|
||||
} else if objectItem, isObjectItem := display.hoverItems.activeItem.(objectHoverItem); isObjectItem {
|
||||
_, _, heightShift := lvl.Size()
|
||||
obj := lvl.Object(objectItem.id)
|
||||
if obj != nil {
|
||||
typeString = fmt.Sprintf("%3d = %v", objectItem.id, obj.Triple())
|
||||
heightInTiles, err := heightShift.ValueFromObjectHeight(obj.Z)
|
||||
if err == nil {
|
||||
floorString = fmt.Sprintf("%2.3f", heightInTiles)
|
||||
}
|
||||
floorRaw = int(obj.Z)
|
||||
hasFloor = true
|
||||
}
|
||||
}
|
||||
}
|
||||
imgui.Text("T: " + typeString)
|
||||
if hasPos {
|
||||
imgui.Text(fmt.Sprintf("X: T %2d F %3d", pos.X.Tile(), pos.X.Fine()))
|
||||
imgui.Text(fmt.Sprintf("Y: T %2d F %3d", pos.Y.Tile(), pos.Y.Fine()))
|
||||
} else {
|
||||
imgui.Text("X: T -- F ---")
|
||||
imgui.Text("Y: T -- F ---")
|
||||
}
|
||||
if hasFloor {
|
||||
imgui.Text(fmt.Sprintf("F: %3d = %s", floorRaw, floorString))
|
||||
} else {
|
||||
imgui.Text("F: -- = --.---")
|
||||
}
|
||||
if hasCeiling {
|
||||
imgui.Text(fmt.Sprintf("C: %3d = %s", ceilingRaw, ceilingString))
|
||||
} else {
|
||||
imgui.Text("C: -- = --.---")
|
||||
}
|
||||
imgui.End()
|
||||
}
|
||||
}
|
||||
|
||||
func (display *MapDisplay) renderContextMenu() {
|
||||
if display.contextMenuRequested {
|
||||
imgui.OpenPopup(contextMenuName)
|
||||
display.contextMenuRequested = false
|
||||
}
|
||||
if imgui.BeginPopupV(contextMenuName, imgui.PopupFlagsMouseButtonRight) {
|
||||
readOnly := display.editor.IsReadOnly()
|
||||
if imgui.BeginMenu("New...") {
|
||||
implicitTriple := display.editor.NewObjectTriple()
|
||||
canCreateImplicitClass := display.canCreateObjectOf(implicitTriple.Class)
|
||||
if imgui.MenuItemV("Object", "Ctrl+2ndClick", false, canCreateImplicitClass) {
|
||||
display.requestCreateNewObject(false, implicitTriple)
|
||||
}
|
||||
if imgui.MenuItemV("Object (at grid)", "Ctrl+Shift+2ndClick", false, canCreateImplicitClass) {
|
||||
display.requestCreateNewObject(true, implicitTriple)
|
||||
}
|
||||
imgui.EndMenu()
|
||||
}
|
||||
imgui.Separator()
|
||||
if imgui.MenuItemV("Delete Objects", "", false, !readOnly && display.editor.HasSelectedObjects()) {
|
||||
_ = display.editor.DeleteObjects()
|
||||
}
|
||||
if imgui.MenuItemV("Clear Tiles", "", false, !readOnly && display.editor.HasSelectedTiles()) {
|
||||
_ = display.editor.ClearTiles()
|
||||
}
|
||||
imgui.EndPopup()
|
||||
}
|
||||
}
|
||||
|
||||
// WindowResized must be called to notify of a change in window geometry.
|
||||
func (display *MapDisplay) WindowResized(width int, height int) {
|
||||
display.context.ProjectionMatrix = mgl.Ortho2D(0.0, float32(width), float32(height), 0.0)
|
||||
display.camera.SetViewportSize(float32(width), float32(height))
|
||||
display.positionPopupPos.X = float32(width) - 10.0
|
||||
display.positionPopupPos.Y = float32(height) - 10.0
|
||||
}
|
||||
|
||||
func (display *MapDisplay) unprojectPixel(pixelX, pixelY float32) (x, y float32) {
|
||||
pixelVec := mgl.Vec4{pixelX, pixelY, 0.0, 1.0}
|
||||
invertedView := display.camera.ViewMatrix().Inv()
|
||||
result := invertedView.Mul4x1(pixelVec)
|
||||
|
||||
return result[0], result[1]
|
||||
}
|
||||
|
||||
// MouseButtonDown must be called when a button was pressed.
|
||||
func (display *MapDisplay) MouseButtonDown(mouseX, mouseY float32, button uint32) {
|
||||
display.updateMouseWorldPosition(mouseX, mouseY)
|
||||
if button == input.MousePrimary {
|
||||
lastPixelX, lastPixelY := mouseX, mouseY
|
||||
|
||||
display.mouseMoved = false
|
||||
display.moveCapture = func(pixelX, pixelY float32) {
|
||||
lastWorldX, lastWorldY := display.unprojectPixel(lastPixelX, lastPixelY)
|
||||
worldX, worldY := display.unprojectPixel(pixelX, pixelY)
|
||||
|
||||
display.camera.MoveBy(worldX-lastWorldX, worldY-lastWorldY)
|
||||
lastPixelX, lastPixelY = pixelX, pixelY
|
||||
display.mouseMoved = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MouseButtonUp must be called when a button was released.
|
||||
func (display *MapDisplay) MouseButtonUp(mouseX, mouseY float32, button uint32, modifier input.Modifier) {
|
||||
display.updateMouseWorldPosition(mouseX, mouseY)
|
||||
if button == input.MousePrimary {
|
||||
display.moveCapture = func(float32, float32) {}
|
||||
if !display.mouseMoved && display.positionValid {
|
||||
switch {
|
||||
case modifier.Has(input.ModControl):
|
||||
display.toggleSelectionAtActiveHoverItem()
|
||||
case modifier.Has(input.ModShift) && (display.selectionReference != nil):
|
||||
firstPos := *display.selectionReference
|
||||
|
||||
fromX := int(firstPos.X)
|
||||
fromY := int(firstPos.Y)
|
||||
toX := int(display.position.X.Tile())
|
||||
toY := int(display.position.Y.Tile())
|
||||
xIncrement := 1
|
||||
yIncrement := 1
|
||||
if fromX > toX {
|
||||
xIncrement = -1
|
||||
}
|
||||
if fromY > toY {
|
||||
yIncrement = -1
|
||||
}
|
||||
toX += xIncrement
|
||||
toY += yIncrement
|
||||
var newList []level.TilePosition
|
||||
for y := fromY; y != toY; y += yIncrement {
|
||||
for x := fromX; x != toX; x += xIncrement {
|
||||
newList = append(newList, level.TilePosition{X: byte(x), Y: byte(y)})
|
||||
}
|
||||
}
|
||||
display.levelSelection.SetCurrentSelectedTiles(newList)
|
||||
display.levelSelection.SetCurrentSelectedObjects(display.objectsInTiles(newList))
|
||||
default:
|
||||
display.setSelectionByActiveHoverItem()
|
||||
}
|
||||
}
|
||||
} else if button == input.MouseSecondary {
|
||||
if modifier.IsClear() {
|
||||
display.contextMenuRequested = true
|
||||
} else if modifier.Has(input.ModControl) {
|
||||
display.requestCreateNewObject(modifier.Has(input.ModShift), display.editor.NewObjectTriple())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (display *MapDisplay) canCreateObjectOf(class object.Class) bool {
|
||||
if display.editor.IsReadOnly() {
|
||||
return false
|
||||
}
|
||||
lvl := display.editor.Level()
|
||||
return lvl.HasRoomForObjectOf(class) && display.positionValid
|
||||
}
|
||||
|
||||
func (display *MapDisplay) requestCreateNewObject(snapToGrid bool, triple object.Triple) {
|
||||
if !display.canCreateObjectOf(triple.Class) {
|
||||
return
|
||||
}
|
||||
pos := display.position
|
||||
if snapToGrid {
|
||||
toGrid := func(value byte) byte {
|
||||
cornerSnapDistance := level.FineCoordinatesPerTileSide / 4
|
||||
switch {
|
||||
case value < byte(cornerSnapDistance):
|
||||
return 0x00
|
||||
case value >= byte(level.FineCoordinatesPerTileSide-cornerSnapDistance):
|
||||
return level.FineCoordinatesPerTileSide - 1
|
||||
default:
|
||||
return level.FineCoordinatesPerTileSide / 2
|
||||
}
|
||||
}
|
||||
pos.X = level.CoordinateAt(pos.X.Tile(), toGrid(pos.X.Fine()))
|
||||
pos.Y = level.CoordinateAt(pos.Y.Tile(), toGrid(pos.Y.Fine()))
|
||||
}
|
||||
err := display.editor.CreateNewObject(triple, func(obj *level.ObjectMainEntry) {
|
||||
obj.X = pos.X
|
||||
obj.Y = pos.Y
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (display *MapDisplay) setSelectionByActiveHoverItem() {
|
||||
var tiles []level.TilePosition
|
||||
var objects []level.ObjectID
|
||||
if display.hoverItems.activeItem != nil {
|
||||
if tileItem, isTile := display.hoverItems.activeItem.(tileHoverItem); isTile {
|
||||
tiles = append(tiles, tileItem.pos.Tile())
|
||||
} else if objectItem, isObject := display.hoverItems.activeItem.(objectHoverItem); isObject {
|
||||
objects = append(objects, objectItem.id)
|
||||
}
|
||||
}
|
||||
display.levelSelection.SetCurrentSelectedTiles(tiles)
|
||||
if len(tiles) > 0 {
|
||||
display.selectionReference = &tiles[0]
|
||||
display.levelSelection.SetCurrentSelectedObjects(display.objectsInTiles(tiles))
|
||||
} else {
|
||||
display.levelSelection.SetCurrentSelectedObjects(objects)
|
||||
}
|
||||
}
|
||||
|
||||
func (display *MapDisplay) toggleSelectionAtActiveHoverItem() {
|
||||
if display.hoverItems.activeItem != nil {
|
||||
if tileItem, isTile := display.hoverItems.activeItem.(tileHoverItem); isTile {
|
||||
wasSelected := display.levelSelection.IsTileSelected(tileItem.pos.Tile())
|
||||
tiles := []level.TilePosition{tileItem.pos.Tile()}
|
||||
if wasSelected {
|
||||
display.levelSelection.RemoveCurrentSelectedTiles(tiles)
|
||||
display.levelSelection.RemoveCurrentSelectedObjects(display.objectsInTiles(tiles))
|
||||
} else {
|
||||
display.levelSelection.AddCurrentSelectedTiles(tiles)
|
||||
display.levelSelection.AddCurrentSelectedObjects(display.objectsInTiles(tiles))
|
||||
}
|
||||
} else if objectItem, isObject := display.hoverItems.activeItem.(objectHoverItem); isObject {
|
||||
display.levelSelection.ToggleObjectSelection([]level.ObjectID{objectItem.id})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (display *MapDisplay) objectsInTiles(tiles []level.TilePosition) []level.ObjectID {
|
||||
tilesContain := func(pos level.TilePosition) bool {
|
||||
for _, entry := range tiles {
|
||||
if entry == pos {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var objects []level.ObjectID
|
||||
lvl := display.editor.Level()
|
||||
lvl.ForEachObject(func(id level.ObjectID, entry level.ObjectMainEntry) {
|
||||
if tilesContain(entry.TilePosition()) {
|
||||
objects = append(objects, id)
|
||||
}
|
||||
})
|
||||
return objects
|
||||
}
|
||||
|
||||
// MouseMoved must be called for a mouse move.
|
||||
func (display *MapDisplay) MouseMoved(mouseX, mouseY float32) {
|
||||
display.updateMouseWorldPosition(mouseX, mouseY)
|
||||
if display.positionValid {
|
||||
display.hoverItems.find(display.editor.Level(), display.position)
|
||||
} else {
|
||||
display.hoverItems.reset()
|
||||
}
|
||||
display.moveCapture(mouseX, mouseY)
|
||||
}
|
||||
|
||||
// MouseScrolled must be called for a mouse scroll.
|
||||
func (display *MapDisplay) MouseScrolled(mouseX, mouseY float32, deltaX, deltaY float32, modifier input.Modifier) {
|
||||
if modifier.Has(input.ModControl) {
|
||||
display.hoverItems.scroll(deltaY > 0)
|
||||
} else {
|
||||
worldX, worldY := display.unprojectPixel(mouseX, mouseY)
|
||||
|
||||
if deltaY < 0 {
|
||||
display.camera.ZoomAt(-0.5, worldX, worldY)
|
||||
}
|
||||
if deltaY > 0 {
|
||||
display.camera.ZoomAt(0.5, worldX, worldY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (display *MapDisplay) updateMouseWorldPosition(mouseX, mouseY float32) {
|
||||
worldX, worldY := display.unprojectPixel(mouseX, mouseY)
|
||||
lvl := display.editor.Level()
|
||||
width, height, _ := lvl.Size()
|
||||
worldWidth := float32(width) * level.FineCoordinatesPerTileSide
|
||||
worldHeight := float32(height) * level.FineCoordinatesPerTileSide
|
||||
|
||||
display.positionValid = (worldX >= 0.0) && (worldX < worldWidth) && (worldY >= 0.0) && (worldY < worldHeight)
|
||||
if display.positionValid {
|
||||
display.position = MapPosition{X: level.Coordinate(worldX + 0.5), Y: level.Coordinate(worldY + 0.5)}
|
||||
} else {
|
||||
display.position = MapPosition{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
mgl "github.com/go-gl/mathgl/mgl32"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
)
|
||||
|
||||
var mapTileGridVertexShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
in vec4 vertexPosition;
|
||||
out float colorAlpha;
|
||||
|
||||
uniform mat4 alphaMatrix;
|
||||
|
||||
uniform mat4 modelMatrix;
|
||||
uniform mat4 viewMatrix;
|
||||
uniform mat4 projectionMatrix;
|
||||
|
||||
void main(void) {
|
||||
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition.xy, 0.0, 1.0);
|
||||
colorAlpha = alphaMatrix[int(vertexPosition.z)][int(vertexPosition.w)];
|
||||
}
|
||||
`
|
||||
|
||||
var mapTileGridFragmentShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
uniform vec4 color;
|
||||
in float colorAlpha;
|
||||
out vec4 fragColor;
|
||||
|
||||
void main(void) {
|
||||
fragColor = vec4(color.rgb, color.a*colorAlpha);
|
||||
}
|
||||
`
|
||||
|
||||
// TileMapper returns basic information to draw a 2D map.
|
||||
type TileMapper interface {
|
||||
MapGridInfo(pos level.TilePosition) (level.TileType, level.TileSlopeControl, level.WallHeights)
|
||||
}
|
||||
|
||||
// MapGrid renders the grid of the map, based on calculated wall heights.
|
||||
type MapGrid struct {
|
||||
context *render.Context
|
||||
|
||||
program uint32
|
||||
vao *opengl.VertexArrayObject
|
||||
vertexPositionBuffer uint32
|
||||
tickVertexPositionBuffer uint32
|
||||
vertexPositionAttrib int32
|
||||
colorUniform opengl.Vector4Uniform
|
||||
|
||||
/*
|
||||
The alpha matrix is set up to specify the alpha value used for the wall sections.
|
||||
There are four cardinal walls with three sections each, plus two diagonal walls (single section), as well
|
||||
as the slope-ticks that re-use the shader.
|
||||
The matrix is meant to be used like this:
|
||||
N N N D1
|
||||
E E E D2
|
||||
S S S -
|
||||
W W W T
|
||||
D1 and D2 being the two diagonals, and T specifying the cell for the tick-marks.
|
||||
The vertex buffers are an array of v4 entries, with z/w specifying the row and column index to take.
|
||||
*/
|
||||
alphaMatrixUniform opengl.Matrix4Uniform
|
||||
modelMatrixUniform opengl.Matrix4Uniform
|
||||
viewMatrixUniform opengl.Matrix4Uniform
|
||||
projectionMatrixUniform opengl.Matrix4Uniform
|
||||
|
||||
slopeTicksByType [][]int
|
||||
floorTickStarts []int32
|
||||
ceilingTickStarts []int32
|
||||
}
|
||||
|
||||
// NewMapGrid returns a new instance.
|
||||
func NewMapGrid(context *render.Context) *MapGrid {
|
||||
gl := context.OpenGL
|
||||
program, programErr := opengl.LinkNewStandardProgram(gl, mapTileGridVertexShaderSource, mapTileGridFragmentShaderSource)
|
||||
if programErr != nil {
|
||||
panic(opengl.NamedShaderError{Name: "MapGrid", Nested: programErr})
|
||||
}
|
||||
grid := &MapGrid{
|
||||
context: context,
|
||||
program: program,
|
||||
vao: opengl.NewVertexArrayObject(gl, program),
|
||||
vertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||
tickVertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||
vertexPositionAttrib: gl.GetAttribLocation(program, "vertexPosition"),
|
||||
colorUniform: opengl.Vector4Uniform(gl.GetUniformLocation(program, "color")),
|
||||
alphaMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "alphaMatrix")),
|
||||
modelMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "modelMatrix")),
|
||||
viewMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "viewMatrix")),
|
||||
projectionMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "projectionMatrix")),
|
||||
}
|
||||
|
||||
{
|
||||
tickVertices := grid.calculateTickVertices()
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, grid.tickVertexPositionBuffer)
|
||||
gl.BufferData(opengl.ARRAY_BUFFER, len(tickVertices)*4, tickVertices, opengl.STATIC_DRAW)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
}
|
||||
{
|
||||
const parts = 3
|
||||
left := float32(-0.5)
|
||||
right := float32(0.5)
|
||||
top := float32(0.5)
|
||||
bottom := float32(-0.5)
|
||||
finePerPart := float32(1.0 / parts)
|
||||
|
||||
var wallVertices []float32
|
||||
for i := 0; i < parts; i++ {
|
||||
wallVertices = append(wallVertices, left+(finePerPart*float32(i)), top, 0.0, float32(i))
|
||||
wallVertices = append(wallVertices, left+(finePerPart*float32(i+1)), top, 0.0, float32(i))
|
||||
}
|
||||
for i := 0; i < parts; i++ {
|
||||
wallVertices = append(wallVertices, right, top-(finePerPart*float32(i)), 1.0, float32(i))
|
||||
wallVertices = append(wallVertices, right, top-(finePerPart*float32(i+1)), 1.0, float32(i))
|
||||
}
|
||||
for i := 0; i < parts; i++ {
|
||||
wallVertices = append(wallVertices, right-(finePerPart*float32(i)), bottom, 2.0, float32(i))
|
||||
wallVertices = append(wallVertices, right-(finePerPart*float32(i+1)), bottom, 2.0, float32(i))
|
||||
}
|
||||
for i := 0; i < parts; i++ {
|
||||
wallVertices = append(wallVertices, left, bottom+(finePerPart*float32(i)), 3.0, float32(i))
|
||||
wallVertices = append(wallVertices, left, bottom+(finePerPart*float32(i+1)), 3.0, float32(i))
|
||||
}
|
||||
wallVertices = append(wallVertices, left, top, 0.0, 3.0)
|
||||
wallVertices = append(wallVertices, right, bottom, 0.0, 3.0)
|
||||
wallVertices = append(wallVertices, right, top, 1.0, 3.0)
|
||||
wallVertices = append(wallVertices, left, bottom, 1.0, 3.0)
|
||||
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, grid.vertexPositionBuffer)
|
||||
gl.BufferData(opengl.ARRAY_BUFFER, len(wallVertices)*4, wallVertices, opengl.STATIC_DRAW)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
}
|
||||
{
|
||||
grid.slopeTicksByType = [][]int{
|
||||
nil,
|
||||
nil,
|
||||
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
|
||||
{0, 1},
|
||||
{1, 2},
|
||||
{2, 3},
|
||||
{3, 0},
|
||||
|
||||
{3, 0, 1},
|
||||
{0, 1, 2},
|
||||
{1, 2, 3},
|
||||
{2, 3, 0},
|
||||
|
||||
{2},
|
||||
{3},
|
||||
{0},
|
||||
{1},
|
||||
}
|
||||
grid.floorTickStarts = []int32{0, 6, 12, 18}
|
||||
grid.ceilingTickStarts = []int32{3, 9, 15, 21}
|
||||
}
|
||||
|
||||
return grid
|
||||
}
|
||||
|
||||
// Dispose releases any internal resources.
|
||||
func (grid *MapGrid) Dispose() {
|
||||
gl := grid.context.OpenGL
|
||||
gl.DeleteProgram(grid.program)
|
||||
gl.DeleteBuffers([]uint32{grid.vertexPositionBuffer, grid.tickVertexPositionBuffer})
|
||||
grid.vao.Dispose()
|
||||
}
|
||||
|
||||
func (grid *MapGrid) calculateTickVertices() []float32 {
|
||||
dotHalf := float32(0.05)
|
||||
dotBase := float32(0.5) - (dotHalf * 2.0)
|
||||
|
||||
top := dotBase + dotHalf
|
||||
topEnd := dotBase - dotHalf
|
||||
left := -dotBase - dotHalf
|
||||
leftEnd := -dotBase + dotHalf
|
||||
|
||||
right := top
|
||||
rightEnd := topEnd
|
||||
bottom := left
|
||||
bottomEnd := leftEnd
|
||||
|
||||
return []float32{
|
||||
left, top, 3.0, 3.0, leftEnd, top, 3.0, 3.0, left, topEnd, 3.0, 3.0,
|
||||
leftEnd, top, 3.0, 3.0, leftEnd, topEnd, 3.0, 3.0, left, topEnd, 3.0, 3.0,
|
||||
|
||||
right, top, 3.0, 3.0, right, topEnd, 3.0, 3.0, rightEnd, top, 3.0, 3.0,
|
||||
rightEnd, top, 3.0, 3.0, right, topEnd, 3.0, 3.0, rightEnd, topEnd, 3.0, 3.0,
|
||||
|
||||
right, bottom, 3.0, 3.0, rightEnd, bottom, 3.0, 3.0, right, bottomEnd, 3.0, 3.0,
|
||||
right, bottomEnd, 3.0, 3.0, rightEnd, bottom, 3.0, 3.0, rightEnd, bottomEnd, 3.0, 3.0,
|
||||
|
||||
left, bottomEnd, 3.0, 3.0, leftEnd, bottom, 3.0, 3.0, left, bottom, 3.0, 3.0,
|
||||
left, bottomEnd, 3.0, 3.0, leftEnd, bottomEnd, 3.0, 3.0, leftEnd, bottom, 3.0, 3.0,
|
||||
}
|
||||
}
|
||||
|
||||
// Render renders the grid.
|
||||
func (grid *MapGrid) Render(columns, rows int, mapper TileMapper) {
|
||||
gl := grid.context.OpenGL
|
||||
|
||||
grid.vao.OnShader(func() {
|
||||
modelMatrix := mgl.Ident4()
|
||||
lineColor := [4]float32{0.0, 0.8, 0.0, 1.0}
|
||||
|
||||
grid.viewMatrixUniform.Set(gl, grid.context.ViewMatrix)
|
||||
grid.projectionMatrixUniform.Set(gl, &grid.context.ProjectionMatrix)
|
||||
|
||||
heightFactor := [32*2 + 1]float32{
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0,
|
||||
0.1, 0.2, 0.3, 0.3, 0.4, 0.4, 0.5, 0.7,
|
||||
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
|
||||
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
|
||||
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
|
||||
}
|
||||
|
||||
for y := 0; y < rows; y++ {
|
||||
for x := 0; x < columns; x++ {
|
||||
tileType, slopeControl, wallHeights := mapper.MapGridInfo(level.TilePosition{X: byte(x), Y: byte(y)})
|
||||
if tileType == level.TileTypeSolid {
|
||||
continue
|
||||
}
|
||||
|
||||
modelMatrix = mgl.Ident4().
|
||||
Mul4(mgl.Translate3D((float32(x)+0.5)*level.FineCoordinatesPerTileSide, (float32(y)+0.5)*level.FineCoordinatesPerTileSide, 0.0)).
|
||||
Mul4(mgl.Scale3D(level.FineCoordinatesPerTileSide, level.FineCoordinatesPerTileSide, 1.0))
|
||||
grid.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||
var alphaMatrix mgl.Mat4
|
||||
alphaMatrix[15] = 1.0 // for tick marks
|
||||
|
||||
gl.EnableVertexAttribArray(uint32(grid.vertexPositionAttrib))
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, grid.vertexPositionBuffer)
|
||||
gl.VertexAttribOffset(uint32(grid.vertexPositionAttrib), 4, opengl.FLOAT, false, 0, 0)
|
||||
|
||||
alphaMatrix[0+0] = heightFactor[int(wallHeights.North[0])+32]
|
||||
alphaMatrix[0+1] = heightFactor[int(wallHeights.North[1])+32]
|
||||
alphaMatrix[0+2] = heightFactor[int(wallHeights.North[2])+32]
|
||||
|
||||
alphaMatrix[4+0] = heightFactor[int(wallHeights.East[0])+32]
|
||||
alphaMatrix[4+1] = heightFactor[int(wallHeights.East[1])+32]
|
||||
alphaMatrix[4+2] = heightFactor[int(wallHeights.East[2])+32]
|
||||
|
||||
alphaMatrix[8+0] = heightFactor[int(wallHeights.South[0])+32]
|
||||
alphaMatrix[8+1] = heightFactor[int(wallHeights.South[1])+32]
|
||||
alphaMatrix[8+2] = heightFactor[int(wallHeights.South[2])+32]
|
||||
|
||||
alphaMatrix[12+0] = heightFactor[int(wallHeights.West[0])+32]
|
||||
alphaMatrix[12+1] = heightFactor[int(wallHeights.West[1])+32]
|
||||
alphaMatrix[12+2] = heightFactor[int(wallHeights.West[2])+32]
|
||||
if tileType == level.TileTypeDiagonalOpenNorthEast || tileType == level.TileTypeDiagonalOpenSouthWest {
|
||||
alphaMatrix[0+3] = 1.0
|
||||
}
|
||||
if tileType == level.TileTypeDiagonalOpenNorthWest || tileType == level.TileTypeDiagonalOpenSouthEast {
|
||||
alphaMatrix[4+3] = 1.0
|
||||
}
|
||||
|
||||
grid.colorUniform.Set(gl, &lineColor)
|
||||
grid.alphaMatrixUniform.Set(gl, &alphaMatrix)
|
||||
gl.DrawArrays(opengl.LINES, 0, 28)
|
||||
|
||||
grid.renderSlopeTicks(gl, tileType, slopeControl)
|
||||
}
|
||||
}
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (grid *MapGrid) renderSlopeTicks(gl opengl.OpenGL, tileType level.TileType, slopeControl level.TileSlopeControl) {
|
||||
var floorTicks []int
|
||||
var ceilingTicks []int
|
||||
|
||||
if slopeControl != level.TileSlopeControlFloorFlat {
|
||||
floorTicks = grid.slopeTicksByType[tileType]
|
||||
}
|
||||
if slopeControl == level.TileSlopeControlCeilingMirrored {
|
||||
ceilingTicks = grid.slopeTicksByType[tileType]
|
||||
} else if slopeControl != level.TileSlopeControlCeilingFlat {
|
||||
ceilingTicks = grid.slopeTicksByType[tileType.Info().SlopeInvertedType]
|
||||
}
|
||||
|
||||
gl.EnableVertexAttribArray(uint32(grid.vertexPositionAttrib))
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, grid.tickVertexPositionBuffer)
|
||||
gl.VertexAttribOffset(uint32(grid.vertexPositionAttrib), 4, opengl.FLOAT, false, 0, 0)
|
||||
|
||||
floorTickColor := [4]float32{0.0, 0.8, 0.0, 0.9}
|
||||
grid.colorUniform.Set(gl, &floorTickColor)
|
||||
for _, index := range floorTicks {
|
||||
gl.DrawArrays(opengl.TRIANGLES, grid.floorTickStarts[index], 3)
|
||||
}
|
||||
ceilingTickColor := [4]float32{0.8, 0.0, 0.0, 0.9}
|
||||
grid.colorUniform.Set(gl, &ceilingTickColor)
|
||||
for _, index := range ceilingTicks {
|
||||
gl.DrawArrays(opengl.TRIANGLES, grid.ceilingTickStarts[index], 3)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
mgl "github.com/go-gl/mathgl/mgl32"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
)
|
||||
|
||||
var mapIconsVertexShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
in vec3 vertexPosition;
|
||||
in vec3 uvPosition;
|
||||
|
||||
uniform mat4 modelMatrix;
|
||||
uniform mat4 viewMatrix;
|
||||
uniform mat4 projectionMatrix;
|
||||
uniform vec4 uvLimit;
|
||||
|
||||
out vec2 uv;
|
||||
|
||||
void main(void) {
|
||||
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
|
||||
|
||||
uv = vec2(uvPosition.x * uvLimit.x, uvPosition.y * uvLimit.y);
|
||||
}
|
||||
`
|
||||
|
||||
var mapIconsFragmentShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
uniform sampler2D palette;
|
||||
uniform sampler2D bitmap;
|
||||
|
||||
in vec2 uv;
|
||||
out vec4 fragColor;
|
||||
|
||||
void main(void) {
|
||||
vec4 pixel = texture(bitmap, uv);
|
||||
|
||||
if (pixel.r > 0.0) {
|
||||
fragColor = texture(palette, vec2(pixel.r, 0.5));
|
||||
} else {
|
||||
discard;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// MapIcons is a renderable for simple bitmaps.
|
||||
type MapIcons struct {
|
||||
context *render.Context
|
||||
|
||||
program uint32
|
||||
vao *opengl.VertexArrayObject
|
||||
vertexPositionBuffer uint32
|
||||
vertexPositionAttrib int32
|
||||
uvPositionBuffer uint32
|
||||
uvPositionAttrib int32
|
||||
modelMatrixUniform opengl.Matrix4Uniform
|
||||
viewMatrixUniform opengl.Matrix4Uniform
|
||||
projectionMatrixUniform opengl.Matrix4Uniform
|
||||
uvLimitUniform opengl.Vector4Uniform
|
||||
|
||||
paletteUniform int32
|
||||
bitmapUniform int32
|
||||
}
|
||||
|
||||
type iconData struct {
|
||||
pos MapPosition
|
||||
texture *graphics.BitmapTexture
|
||||
}
|
||||
|
||||
// NewMapIcons returns a new instance.
|
||||
func NewMapIcons(context *render.Context) *MapIcons {
|
||||
gl := context.OpenGL
|
||||
program, programErr := opengl.LinkNewStandardProgram(gl, mapIconsVertexShaderSource, mapIconsFragmentShaderSource)
|
||||
|
||||
if programErr != nil {
|
||||
panic(opengl.NamedShaderError{Name: "MapIconsShader", Nested: programErr})
|
||||
}
|
||||
renderable := &MapIcons{
|
||||
context: context,
|
||||
program: program,
|
||||
|
||||
vao: opengl.NewVertexArrayObject(gl, program),
|
||||
vertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||
vertexPositionAttrib: gl.GetAttribLocation(program, "vertexPosition"),
|
||||
uvPositionBuffer: gl.GenBuffers(1)[0],
|
||||
uvPositionAttrib: gl.GetAttribLocation(program, "uvPosition"),
|
||||
modelMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "modelMatrix")),
|
||||
viewMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "viewMatrix")),
|
||||
projectionMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "projectionMatrix")),
|
||||
uvLimitUniform: opengl.Vector4Uniform(gl.GetUniformLocation(program, "uvLimit")),
|
||||
|
||||
paletteUniform: gl.GetUniformLocation(program, "palette"),
|
||||
bitmapUniform: gl.GetUniformLocation(program, "bitmap")}
|
||||
|
||||
{
|
||||
half := float32(0.5)
|
||||
var vertices = []float32{
|
||||
-half, half, 0.0,
|
||||
half, half, 0.0,
|
||||
half, -half, 0.0,
|
||||
|
||||
half, -half, 0.0,
|
||||
-half, -half, 0.0,
|
||||
-half, half, 0.0,
|
||||
}
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexPositionBuffer)
|
||||
gl.BufferData(opengl.ARRAY_BUFFER, len(vertices)*4, vertices, opengl.STATIC_DRAW)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
}
|
||||
{
|
||||
var uv = []float32{
|
||||
0.0, 0.0, 0.0,
|
||||
1.0, 0.0, 0.0,
|
||||
1.0, 1.0, 0.0,
|
||||
|
||||
1.0, 1.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0,
|
||||
}
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.uvPositionBuffer)
|
||||
gl.BufferData(opengl.ARRAY_BUFFER, len(uv)*4, uv, opengl.STATIC_DRAW)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
}
|
||||
renderable.vao.WithSetter(func(gl opengl.OpenGL) {
|
||||
gl.EnableVertexAttribArray(uint32(renderable.vertexPositionAttrib))
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexPositionBuffer)
|
||||
gl.VertexAttribOffset(uint32(renderable.vertexPositionAttrib), 3, opengl.FLOAT, false, 0, 0)
|
||||
gl.EnableVertexAttribArray(uint32(renderable.uvPositionAttrib))
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.uvPositionBuffer)
|
||||
gl.VertexAttribOffset(uint32(renderable.uvPositionAttrib), 3, opengl.FLOAT, false, 0, 0)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
})
|
||||
|
||||
return renderable
|
||||
}
|
||||
|
||||
// Render renders the icons with their center at given position.
|
||||
func (renderable *MapIcons) Render(paletteTexture *graphics.PaletteTexture, iconSize float32, icons []iconData) {
|
||||
gl := renderable.context.OpenGL
|
||||
|
||||
renderable.vao.OnShader(func() {
|
||||
renderable.viewMatrixUniform.Set(gl, renderable.context.ViewMatrix)
|
||||
renderable.projectionMatrixUniform.Set(gl, &renderable.context.ProjectionMatrix)
|
||||
|
||||
textureUnit := int32(0)
|
||||
gl.ActiveTexture(opengl.TEXTURE0 + uint32(textureUnit))
|
||||
gl.BindTexture(opengl.TEXTURE_2D, paletteTexture.Handle())
|
||||
gl.Uniform1i(renderable.paletteUniform, textureUnit)
|
||||
|
||||
textureUnit = 1
|
||||
gl.ActiveTexture(opengl.TEXTURE0 + uint32(textureUnit))
|
||||
gl.Uniform1i(renderable.bitmapUniform, textureUnit)
|
||||
for _, icon := range icons {
|
||||
x, y := float32(icon.pos.X), float32(icon.pos.Y)
|
||||
u, v := icon.texture.UV()
|
||||
width, height := renderable.limitedSize(iconSize, icon.texture)
|
||||
modelMatrix := mgl.Ident4().
|
||||
Mul4(mgl.Translate3D(x, y, 0.0)).
|
||||
Mul4(mgl.Scale3D(width, height, 1.0))
|
||||
|
||||
renderable.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||
|
||||
uvLimit := [4]float32{u, v, 0, 0}
|
||||
renderable.uvLimitUniform.Set(gl, &uvLimit)
|
||||
|
||||
gl.BindTexture(opengl.TEXTURE_2D, icon.texture.Handle())
|
||||
gl.DrawArrays(opengl.TRIANGLES, 0, 6)
|
||||
}
|
||||
gl.BindTexture(opengl.TEXTURE_2D, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (renderable *MapIcons) limitedSize(iconSize float32, texture *graphics.BitmapTexture) (width, height float32) {
|
||||
width, height = texture.Size()
|
||||
referenceSize := float32(16.0)
|
||||
larger := width
|
||||
|
||||
if larger < height {
|
||||
larger = height
|
||||
}
|
||||
if larger > referenceSize {
|
||||
ratio := referenceSize / larger
|
||||
width *= ratio
|
||||
height *= ratio
|
||||
}
|
||||
factor := iconSize / referenceSize
|
||||
width *= factor
|
||||
height *= factor
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package levels
|
||||
|
||||
import "github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
|
||||
// MapPosition describes a specific two-dimensional point on the map.
|
||||
type MapPosition struct {
|
||||
X level.Coordinate
|
||||
Y level.Coordinate
|
||||
}
|
||||
|
||||
// Tile returns the tile position of the exact position.
|
||||
func (pos MapPosition) Tile() level.TilePosition {
|
||||
return level.TilePosition{X: pos.X.Tile(), Y: pos.Y.Tile()}
|
||||
}
|
||||
|
||||
// Fine returns the fine position of the exact position.
|
||||
func (pos MapPosition) Fine() level.FinePosition {
|
||||
return level.FinePosition{X: pos.X.Fine(), Y: pos.Y.Fine()}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
mgl "github.com/go-gl/mathgl/mgl32"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
)
|
||||
|
||||
var mapTexturesVertexShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
in vec3 vertexPosition;
|
||||
|
||||
uniform mat4 modelMatrix;
|
||||
uniform mat4 viewMatrix;
|
||||
uniform mat4 projectionMatrix;
|
||||
uniform mat4 uvMatrix;
|
||||
|
||||
out vec2 uv;
|
||||
|
||||
void main(void) {
|
||||
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
|
||||
|
||||
uv = (uvMatrix * vec4(vertexPosition, 1.0)).xy;
|
||||
}
|
||||
`
|
||||
|
||||
var mapTexturesFragmentShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
in vec2 uv;
|
||||
|
||||
uniform sampler2D palette;
|
||||
uniform sampler2D bitmap;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
void main(void) {
|
||||
vec4 pixel = texture(bitmap, uv);
|
||||
|
||||
fragColor = texture(palette, vec2(pixel.r, 0.5));
|
||||
}
|
||||
`
|
||||
|
||||
// TextureQuery is a getter function to retrieve the texture for the given
|
||||
// level texture index.
|
||||
type TextureQuery func(index level.TextureIndex) (*graphics.BitmapTexture, error)
|
||||
|
||||
// TileTextureQuery is a getter function to retrieve properties for rendering a texture of a tile.
|
||||
type TileTextureQuery func(pos level.TilePosition) (tileType level.TileType, textureIndex level.TextureIndex, textureRotations int)
|
||||
|
||||
// MapTextures is a renderable for textures.
|
||||
type MapTextures struct {
|
||||
context *render.Context
|
||||
|
||||
program uint32
|
||||
vao *opengl.VertexArrayObject
|
||||
vertexPositionBuffer uint32
|
||||
vertexPositionAttrib int32
|
||||
modelMatrixUniform opengl.Matrix4Uniform
|
||||
viewMatrixUniform opengl.Matrix4Uniform
|
||||
projectionMatrixUniform opengl.Matrix4Uniform
|
||||
uvMatrixUniform opengl.Matrix4Uniform
|
||||
|
||||
paletteUniform int32
|
||||
bitmapUniform int32
|
||||
|
||||
textureQuery TextureQuery
|
||||
|
||||
lastTileType level.TileType
|
||||
}
|
||||
|
||||
type tileTypeVertices struct {
|
||||
vertices []float32
|
||||
equivalentType level.TileType
|
||||
}
|
||||
|
||||
var tileTypeVerticesLookup []tileTypeVertices
|
||||
var uvRotations [4]*mgl.Mat4
|
||||
|
||||
func init() {
|
||||
for i := 0; i < 4; i++ {
|
||||
matrix := mgl.Translate3D(0.5, 0.5, 0.0).
|
||||
Mul4(mgl.HomogRotate3DZ(math.Pi * float32(i) / -2.0)).
|
||||
Mul4(mgl.Translate3D(-0.5, -0.5, 0.0)).
|
||||
Mul4(mgl.Scale3D(1.0, -1.0, 1.0))
|
||||
uvRotations[i] = &matrix
|
||||
}
|
||||
|
||||
for _, tileType := range level.TileTypes() {
|
||||
for int(tileType) >= len(tileTypeVerticesLookup) {
|
||||
tileTypeVerticesLookup = append(tileTypeVerticesLookup, tileTypeVertices{equivalentType: level.TileTypeSolid})
|
||||
}
|
||||
switch tileType {
|
||||
case level.TileTypeSolid:
|
||||
case level.TileTypeDiagonalOpenNorthEast:
|
||||
tileTypeVerticesLookup[int(tileType)] = tileTypeVertices{
|
||||
vertices: []float32{
|
||||
0.0, 1.0, 0.0,
|
||||
1.0, 1.0, 0.0,
|
||||
1.0, 0.0, 0.0,
|
||||
},
|
||||
equivalentType: level.TileTypeDiagonalOpenNorthEast,
|
||||
}
|
||||
case level.TileTypeDiagonalOpenNorthWest:
|
||||
tileTypeVerticesLookup[int(tileType)] = tileTypeVertices{
|
||||
vertices: []float32{
|
||||
0.0, 1.0, 0.0,
|
||||
1.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0,
|
||||
},
|
||||
equivalentType: level.TileTypeDiagonalOpenNorthWest,
|
||||
}
|
||||
case level.TileTypeDiagonalOpenSouthEast:
|
||||
tileTypeVerticesLookup[int(tileType)] = tileTypeVertices{
|
||||
vertices: []float32{
|
||||
1.0, 1.0, 0.0,
|
||||
1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0,
|
||||
},
|
||||
equivalentType: level.TileTypeDiagonalOpenSouthEast,
|
||||
}
|
||||
case level.TileTypeDiagonalOpenSouthWest:
|
||||
tileTypeVerticesLookup[int(tileType)] = tileTypeVertices{
|
||||
vertices: []float32{
|
||||
0.0, 1.0, 0.0,
|
||||
1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0,
|
||||
},
|
||||
equivalentType: level.TileTypeDiagonalOpenSouthWest,
|
||||
}
|
||||
default:
|
||||
tileTypeVerticesLookup[int(tileType)] = tileTypeVertices{
|
||||
vertices: []float32{
|
||||
0.0, 0.0, 0.0,
|
||||
1.0, 0.0, 0.0,
|
||||
1.0, 1.0, 0.0,
|
||||
|
||||
1.0, 1.0, 0.0,
|
||||
0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0,
|
||||
},
|
||||
equivalentType: level.TileTypeOpen,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewMapTextures returns a new instance of a renderable for tile map textures.
|
||||
func NewMapTextures(context *render.Context, textureQuery TextureQuery) *MapTextures {
|
||||
gl := context.OpenGL
|
||||
program, programErr := opengl.LinkNewStandardProgram(gl, mapTexturesVertexShaderSource, mapTexturesFragmentShaderSource)
|
||||
if programErr != nil {
|
||||
panic(opengl.NamedShaderError{Name: "MapTexturesShader", Nested: programErr})
|
||||
}
|
||||
renderable := &MapTextures{
|
||||
context: context,
|
||||
program: program,
|
||||
|
||||
vao: opengl.NewVertexArrayObject(gl, program),
|
||||
vertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||
vertexPositionAttrib: gl.GetAttribLocation(program, "vertexPosition"),
|
||||
modelMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "modelMatrix")),
|
||||
viewMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "viewMatrix")),
|
||||
projectionMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "projectionMatrix")),
|
||||
uvMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "uvMatrix")),
|
||||
paletteUniform: gl.GetUniformLocation(program, "palette"),
|
||||
bitmapUniform: gl.GetUniformLocation(program, "bitmap"),
|
||||
textureQuery: textureQuery,
|
||||
lastTileType: level.TileTypeSolid,
|
||||
}
|
||||
|
||||
renderable.vao.WithSetter(func(gl opengl.OpenGL) {
|
||||
gl.EnableVertexAttribArray(uint32(renderable.vertexPositionAttrib))
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexPositionBuffer)
|
||||
gl.VertexAttribOffset(uint32(renderable.vertexPositionAttrib), 3, opengl.FLOAT, false, 0, 0)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
})
|
||||
|
||||
return renderable
|
||||
}
|
||||
|
||||
// Dispose releases any internal resources.
|
||||
func (renderable *MapTextures) Dispose() {
|
||||
gl := renderable.context.OpenGL
|
||||
|
||||
renderable.vao.Dispose()
|
||||
gl.DeleteProgram(renderable.program)
|
||||
gl.DeleteBuffers([]uint32{renderable.vertexPositionBuffer})
|
||||
}
|
||||
|
||||
// Render renders the textures.
|
||||
func (renderable *MapTextures) Render(columns, rows int, tileTextureQuery TileTextureQuery, paletteTexture *graphics.PaletteTexture) {
|
||||
gl := renderable.context.OpenGL
|
||||
|
||||
renderable.vao.OnShader(func() {
|
||||
renderable.viewMatrixUniform.Set(gl, renderable.context.ViewMatrix)
|
||||
renderable.projectionMatrixUniform.Set(gl, &renderable.context.ProjectionMatrix)
|
||||
|
||||
textureUnit := int32(0)
|
||||
gl.ActiveTexture(opengl.TEXTURE0 + uint32(textureUnit))
|
||||
gl.BindTexture(opengl.TEXTURE_2D, paletteTexture.Handle())
|
||||
gl.Uniform1i(renderable.paletteUniform, textureUnit)
|
||||
|
||||
textureUnit = 1
|
||||
gl.ActiveTexture(opengl.TEXTURE0 + uint32(textureUnit))
|
||||
gl.Uniform1i(renderable.bitmapUniform, textureUnit)
|
||||
|
||||
scaling := mgl.Scale3D(level.FineCoordinatesPerTileSide, level.FineCoordinatesPerTileSide, 1.0)
|
||||
for y := 0; y < rows; y++ {
|
||||
for x := 0; x < columns; x++ {
|
||||
tileType, textureIndex, textureRotations := tileTextureQuery(level.TilePosition{X: byte(x), Y: byte(y)})
|
||||
if tileType == level.TileTypeSolid {
|
||||
continue
|
||||
}
|
||||
texture, _ := renderable.textureQuery(textureIndex)
|
||||
if texture == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
modelMatrix := mgl.Translate3D(float32(x)*level.FineCoordinatesPerTileSide, float32(y)*level.FineCoordinatesPerTileSide, 0.0).
|
||||
Mul4(scaling)
|
||||
|
||||
uvMatrix := uvRotations[textureRotations]
|
||||
renderable.uvMatrixUniform.Set(gl, uvMatrix)
|
||||
renderable.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||
gl.BindTexture(opengl.TEXTURE_2D, texture.Handle())
|
||||
renderable.renderTileType(tileType)
|
||||
}
|
||||
}
|
||||
|
||||
gl.BindTexture(opengl.TEXTURE_2D, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func (renderable *MapTextures) renderTileType(tileType level.TileType) {
|
||||
info := tileTypeVerticesLookup[int(tileType)]
|
||||
vertexCount := len(info.vertices) / 3
|
||||
gl := renderable.context.OpenGL
|
||||
if info.equivalentType != renderable.lastTileType {
|
||||
renderable.lastTileType = info.equivalentType
|
||||
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, renderable.vertexPositionBuffer)
|
||||
gl.BufferData(opengl.ARRAY_BUFFER, len(info.vertices)*4, info.vertices, opengl.STATIC_DRAW)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
}
|
||||
gl.DrawArrays(opengl.TRIANGLES, 0, int32(vertexCount))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
package levels
|
||||
|
||||
type objectsViewModel struct {
|
||||
restoreFocus bool
|
||||
windowOpen bool
|
||||
}
|
||||
|
||||
func freshObjectsViewModel() objectsViewModel {
|
||||
return objectsViewModel{}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
)
|
||||
|
||||
// TextureDisplay is an enumeration which texture to display in a 2D map view.
|
||||
type TextureDisplay int
|
||||
|
||||
// TextureDisplay constants are listed below.
|
||||
const (
|
||||
TextureDisplayFloor TextureDisplay = 0
|
||||
TextureDisplayWall TextureDisplay = 1
|
||||
TextureDisplayCeiling TextureDisplay = 2
|
||||
)
|
||||
|
||||
// String returns a textual representation.
|
||||
func (display TextureDisplay) String() string {
|
||||
switch display {
|
||||
case TextureDisplayFloor:
|
||||
return "Floor"
|
||||
case TextureDisplayWall:
|
||||
return "Wall"
|
||||
case TextureDisplayCeiling:
|
||||
return "Ceiling"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown%d", int(display))
|
||||
}
|
||||
}
|
||||
|
||||
// TextureDisplays returns all TextureDisplay constants.
|
||||
func TextureDisplays() []TextureDisplay {
|
||||
return []TextureDisplay{TextureDisplayFloor, TextureDisplayWall, TextureDisplayCeiling}
|
||||
}
|
||||
|
||||
// TextureDisplayFunc is one that resolves texture properties.
|
||||
type TextureDisplayFunc func(*level.TileMapEntry) (atlasIndex level.AtlasIndex, textureRotations int)
|
||||
|
||||
// FloorTexture returns the properties for the floor.
|
||||
func FloorTexture(tile *level.TileMapEntry) (atlasIndex level.AtlasIndex, textureRotations int) {
|
||||
return tile.TextureInfo.FloorTextureIndex(), tile.Floor.TextureRotations()
|
||||
}
|
||||
|
||||
// CeilingTexture returns the properties for the ceiling.
|
||||
func CeilingTexture(tile *level.TileMapEntry) (atlasIndex level.AtlasIndex, textureRotations int) {
|
||||
return tile.TextureInfo.CeilingTextureIndex(), tile.Ceiling.TextureRotations()
|
||||
}
|
||||
|
||||
// WallTexture returns the properties for the floor.
|
||||
func WallTexture(tile *level.TileMapEntry) (atlasIndex level.AtlasIndex, textureRotations int) {
|
||||
return tile.TextureInfo.WallTextureIndex(), 0
|
||||
}
|
||||
|
||||
// Func returns the display func for the current display setting.
|
||||
func (display TextureDisplay) Func() TextureDisplayFunc {
|
||||
if display == TextureDisplayFloor {
|
||||
return FloorTexture
|
||||
} else if display == TextureDisplayCeiling {
|
||||
return CeilingTexture
|
||||
}
|
||||
return WallTexture
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/editor/values"
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||
"github.com/inkyblackness/hacked/ss1/edit"
|
||||
"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"
|
||||
)
|
||||
|
||||
// TilesView is for tile properties.
|
||||
type TilesView struct {
|
||||
editor *edit.LevelEditorService
|
||||
textCache *text.Cache
|
||||
textureCache *graphics.TextureCache
|
||||
|
||||
guiScale float32
|
||||
registry cmd.Registry
|
||||
model tilesViewModel
|
||||
}
|
||||
|
||||
// NewTilesView returns a new instance.
|
||||
func NewTilesView(editor *edit.LevelEditorService,
|
||||
guiScale float32, textCache *text.Cache, textureCache *graphics.TextureCache, registry cmd.Registry) *TilesView {
|
||||
view := &TilesView{
|
||||
editor: editor,
|
||||
textCache: textCache,
|
||||
textureCache: textureCache,
|
||||
|
||||
guiScale: guiScale,
|
||||
model: freshTilesViewModel(),
|
||||
registry: registry,
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// WindowOpen returns the flag address, to be used with the main menu.
|
||||
func (view *TilesView) WindowOpen() *bool {
|
||||
return &view.model.windowOpen
|
||||
}
|
||||
|
||||
// TextureDisplay returns the current setting which textures should be displayed.
|
||||
func (view TilesView) TextureDisplay() TextureDisplay {
|
||||
return view.model.textureDisplay
|
||||
}
|
||||
|
||||
// ColorDisplay returns the current setting which colors should be displayed.
|
||||
func (view TilesView) ColorDisplay() ColorDisplay {
|
||||
lvl := view.editor.Level()
|
||||
if lvl.IsCyberspace() {
|
||||
return view.model.cyberColorDisplay
|
||||
}
|
||||
return view.model.shadowDisplay
|
||||
}
|
||||
|
||||
// Render renders the view.
|
||||
func (view *TilesView) Render() {
|
||||
if view.model.restoreFocus {
|
||||
imgui.SetNextWindowFocus()
|
||||
view.model.restoreFocus = false
|
||||
view.model.windowOpen = true
|
||||
}
|
||||
if view.model.windowOpen {
|
||||
lvl := view.editor.Level()
|
||||
tiles := view.editor.Tiles()
|
||||
readOnly := view.editor.IsReadOnly()
|
||||
title := fmt.Sprintf("Level Tiles, %d selected", len(tiles))
|
||||
if readOnly {
|
||||
title += hintReadOnly
|
||||
}
|
||||
imgui.SetNextWindowSizeV(imgui.Vec2{X: 400 * view.guiScale, Y: 500 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||
if imgui.BeginV(title+"###Level Tiles", view.WindowOpen(), 0) {
|
||||
view.renderContent(lvl, tiles, readOnly)
|
||||
}
|
||||
imgui.End()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *TilesView) renderContent(lvl *level.Level, tiles []*level.TileMapEntry, readOnly bool) {
|
||||
isCyberspace := lvl.IsCyberspace()
|
||||
tileTypeUnifier := values.NewUnifier()
|
||||
floorHeightUnifier := values.NewUnifier()
|
||||
ceilingHeightUnifier := values.NewUnifier()
|
||||
slopeHeightUnifier := values.NewUnifier()
|
||||
slopeControlUnifier := values.NewUnifier()
|
||||
musicZoneUnifier := values.NewUnifier()
|
||||
|
||||
floorPaletteIndexUnifier := values.NewUnifier()
|
||||
ceilingPaletteIndexUnifier := values.NewUnifier()
|
||||
flightPullTypeUnifier := values.NewUnifier()
|
||||
gameOfLightStateUnifier := values.NewUnifier()
|
||||
|
||||
floorTextureIndexUnifier := values.NewUnifier()
|
||||
floorTextureRotationsUnifier := values.NewUnifier()
|
||||
ceilingTextureIndexUnifier := values.NewUnifier()
|
||||
ceilingTextureRotationsUnifier := values.NewUnifier()
|
||||
wallTextureIndexUnifier := values.NewUnifier()
|
||||
wallTextureOffsetUnifier := values.NewUnifier()
|
||||
useAdjacentWallTextureUnifier := values.NewUnifier()
|
||||
wallTexturePatternUnifier := values.NewUnifier()
|
||||
floorLightUnifier := values.NewUnifier()
|
||||
floorLightDeltaUnifier := values.NewUnifier()
|
||||
ceilingLightUnifier := values.NewUnifier()
|
||||
ceilingLightDeltaUnifier := values.NewUnifier()
|
||||
deconstructedUnifier := values.NewUnifier()
|
||||
perildUnifier := values.NewUnifier()
|
||||
floorHazardUnifier := values.NewUnifier()
|
||||
ceilingHazardUnifier := values.NewUnifier()
|
||||
|
||||
for _, tile := range tiles {
|
||||
tileTypeUnifier.Add(tile.Type)
|
||||
floorHeightUnifier.Add(tile.Floor.AbsoluteHeight())
|
||||
ceilingHeightUnifier.Add(tile.Ceiling.AbsoluteHeight())
|
||||
slopeHeightUnifier.Add(tile.SlopeHeight)
|
||||
slopeControlUnifier.Add(tile.Flags.SlopeControl())
|
||||
musicZoneUnifier.Add(tile.Flags.MusicZone())
|
||||
perildUnifier.Add(tile.Flags.Peril())
|
||||
if isCyberspace {
|
||||
floorPaletteIndexUnifier.Add(tile.TextureInfo.FloorPaletteIndex())
|
||||
ceilingPaletteIndexUnifier.Add(tile.TextureInfo.CeilingPaletteIndex())
|
||||
flightPullTypeUnifier.Add(tile.Flags.ForCyberspace().FlightPull())
|
||||
gameOfLightStateUnifier.Add(tile.Flags.ForCyberspace().GameOfLifeState())
|
||||
} else {
|
||||
flags := tile.Flags.ForRealWorld()
|
||||
floorTextureIndexUnifier.Add(int(tile.TextureInfo.FloorTextureIndex()))
|
||||
floorTextureRotationsUnifier.Add(tile.Floor.TextureRotations())
|
||||
ceilingTextureIndexUnifier.Add(int(tile.TextureInfo.CeilingTextureIndex()))
|
||||
ceilingTextureRotationsUnifier.Add(tile.Ceiling.TextureRotations())
|
||||
wallTextureIndexUnifier.Add(int(tile.TextureInfo.WallTextureIndex()))
|
||||
wallTextureOffsetUnifier.Add(flags.WallTextureOffset())
|
||||
useAdjacentWallTextureUnifier.Add(flags.UseAdjacentWallTexture())
|
||||
wallTexturePatternUnifier.Add(flags.WallTexturePattern())
|
||||
floorLightUnifier.Add(level.GradesOfShadow - 1 - flags.FloorShadow())
|
||||
floorLightDeltaUnifier.Add(tile.LightDelta.OfFloor())
|
||||
ceilingLightUnifier.Add(level.GradesOfShadow - 1 - flags.CeilingShadow())
|
||||
ceilingLightDeltaUnifier.Add(tile.LightDelta.OfCeiling())
|
||||
deconstructedUnifier.Add(flags.Deconstructed())
|
||||
floorHazardUnifier.Add(tile.Floor.HasHazard())
|
||||
ceilingHazardUnifier.Add(tile.Ceiling.HasHazard())
|
||||
}
|
||||
}
|
||||
|
||||
imgui.PushItemWidth(-250 * view.guiScale)
|
||||
|
||||
_, _, levelHeight := lvl.Size()
|
||||
tileHeightFormatter := tileHeightFormatterFor(levelHeight)
|
||||
|
||||
tileTypes := level.TileTypes()
|
||||
values.RenderUnifiedCombo(readOnly, "Tile Type", tileTypeUnifier,
|
||||
func(u values.Unifier) int { return int(u.Unified().(level.TileType)) },
|
||||
func(value int) string { return tileTypes[value].String() },
|
||||
len(tileTypes),
|
||||
func(newValue int) { view.changeTiles(setTileTypeTo(level.TileType(newValue))) })
|
||||
values.RenderUnifiedSliderInt(readOnly, "Floor Height", floorHeightUnifier,
|
||||
func(u values.Unifier) int { return int(u.Unified().(level.TileHeightUnit)) },
|
||||
tileHeightFormatter,
|
||||
0, int(level.TileHeightUnitMax)-1,
|
||||
func(newValue int) { view.changeTiles(setFloorHeightTo(level.TileHeightUnit(newValue))) })
|
||||
values.RenderUnifiedSliderInt(readOnly, "Ceiling Height (abs)", ceilingHeightUnifier,
|
||||
func(u values.Unifier) int { return int(u.Unified().(level.TileHeightUnit)) },
|
||||
tileHeightFormatter,
|
||||
1, int(level.TileHeightUnitMax),
|
||||
func(newValue int) { view.changeTiles(setCeilingHeightTo(level.TileHeightUnit(newValue))) })
|
||||
values.RenderUnifiedSliderInt(readOnly, "Slope Height", slopeHeightUnifier,
|
||||
func(u values.Unifier) int { return int(u.Unified().(level.TileHeightUnit)) },
|
||||
tileHeightFormatter,
|
||||
0, int(level.TileHeightUnitMax)-1,
|
||||
func(newValue int) { view.changeTiles(setSlopeHeightTo(level.TileHeightUnit(newValue))) })
|
||||
slopeControls := level.TileSlopeControls()
|
||||
values.RenderUnifiedCombo(readOnly, "Slope Control", slopeControlUnifier,
|
||||
func(u values.Unifier) int { return int(u.Unified().(level.TileSlopeControl)) },
|
||||
func(value int) string { return slopeControls[value].String() },
|
||||
len(slopeControls),
|
||||
func(newValue int) { view.changeTiles(setSlopeControlTo(slopeControls[newValue])) })
|
||||
musicZones := level.TileMusicZones()
|
||||
values.RenderUnifiedCombo(readOnly, "Music Zone", musicZoneUnifier,
|
||||
func(u values.Unifier) int { return int(u.Unified().(level.TileMusicZone)) },
|
||||
func(value int) string { return musicZones[value].String() },
|
||||
len(musicZones),
|
||||
func(newValue int) { view.changeTiles(setMusicZoneTo(musicZones[newValue])) })
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "Music Peril", perildUnifier,
|
||||
func(newValue bool) { view.changeTiles(setPerilTo(newValue)) })
|
||||
|
||||
imgui.Separator()
|
||||
|
||||
if isCyberspace {
|
||||
if imgui.BeginCombo("Color View", view.model.cyberColorDisplay.String()) {
|
||||
displays := ColorDisplays()
|
||||
for _, display := range displays {
|
||||
displayString := display.String()
|
||||
|
||||
if imgui.SelectableV(displayString, display == view.model.cyberColorDisplay, 0, imgui.Vec2{}) {
|
||||
view.model.cyberColorDisplay = display
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
|
||||
values.RenderUnifiedSliderInt(readOnly, "Floor Color", floorPaletteIndexUnifier,
|
||||
func(u values.Unifier) int { return int(u.Unified().(byte)) },
|
||||
func(value int) string { return "%d" },
|
||||
0, bitmap.PaletteSize-1,
|
||||
func(newValue int) { view.changeTiles(setFloorPaletteIndexTo(newValue)) })
|
||||
values.RenderUnifiedSliderInt(readOnly, "Ceiling Color", ceilingPaletteIndexUnifier,
|
||||
func(u values.Unifier) int { return int(u.Unified().(byte)) },
|
||||
func(value int) string { return "%d" },
|
||||
0, bitmap.PaletteSize-1,
|
||||
func(newValue int) { view.changeTiles(setCeilingPaletteIndexTo(newValue)) })
|
||||
|
||||
imgui.Separator()
|
||||
|
||||
flightPulls := level.CyberspaceFlightPulls()
|
||||
values.RenderUnifiedCombo(readOnly, "Flight Pull Type", flightPullTypeUnifier,
|
||||
func(u values.Unifier) int { return int(u.Unified().(level.CyberspaceFlightPull)) },
|
||||
func(value int) string { return flightPulls[value].String() },
|
||||
len(flightPulls),
|
||||
func(newValue int) { view.changeTiles(setFlightPullTypeTo(flightPulls[newValue])) })
|
||||
values.RenderUnifiedSliderInt(readOnly, "Game Of Life State", gameOfLightStateUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string { return "%d" },
|
||||
0, 3,
|
||||
func(newValue int) { view.changeTiles(setGameOfLightStateTo(newValue)) })
|
||||
} else {
|
||||
atlas := lvl.TextureAtlas()
|
||||
|
||||
if imgui.BeginCombo("Texture View", view.model.textureDisplay.String()) {
|
||||
displays := TextureDisplays()
|
||||
for _, display := range displays {
|
||||
displayString := display.String()
|
||||
|
||||
if imgui.SelectableV(displayString, display == view.model.textureDisplay, 0, imgui.Vec2{}) {
|
||||
view.model.textureDisplay = display
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
|
||||
values.RenderUnifiedSliderInt(readOnly, "Floor Texture (atlas index)", floorTextureIndexUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string { return "%d" },
|
||||
0, level.FloorCeilingTextureLimit-1,
|
||||
func(newValue int) { view.changeTiles(setFloorTextureIndexTo(level.AtlasIndex(newValue))) })
|
||||
view.renderTextureSelector(readOnly, "Floor Texture", floorTextureIndexUnifier, atlas, 0, level.FloorCeilingTextureLimit-1,
|
||||
func(newValue int) { view.changeTiles(setFloorTextureIndexTo(level.AtlasIndex(newValue))) })
|
||||
values.RenderUnifiedSliderInt(readOnly, "Floor Texture Rotations", floorTextureRotationsUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string { return "%d" },
|
||||
0, 3,
|
||||
func(newValue int) { view.changeTiles(setFloorTextureRotationsTo(newValue)) })
|
||||
|
||||
values.RenderUnifiedSliderInt(readOnly, "Ceiling Texture (atlas index)", ceilingTextureIndexUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string { return "%d" },
|
||||
0, level.FloorCeilingTextureLimit-1,
|
||||
func(newValue int) { view.changeTiles(setCeilingTextureIndexTo(level.AtlasIndex(newValue))) })
|
||||
view.renderTextureSelector(readOnly, "Ceiling Texture", ceilingTextureIndexUnifier, atlas, 0, level.FloorCeilingTextureLimit-1,
|
||||
func(newValue int) { view.changeTiles(setCeilingTextureIndexTo(level.AtlasIndex(newValue))) })
|
||||
values.RenderUnifiedSliderInt(readOnly, "Ceiling Texture Rotations", ceilingTextureRotationsUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string { return "%d" },
|
||||
0, 3,
|
||||
func(newValue int) { view.changeTiles(setCeilingTextureRotationsTo(newValue)) })
|
||||
|
||||
values.RenderUnifiedSliderInt(readOnly, "Wall Texture (atlas index)", wallTextureIndexUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string { return "%d" },
|
||||
0, len(atlas)-1,
|
||||
func(newValue int) { view.changeTiles(setWallTextureIndexTo(level.AtlasIndex(newValue))) })
|
||||
view.renderTextureSelector(readOnly, "Wall Texture", wallTextureIndexUnifier, atlas, 0, len(atlas)-1,
|
||||
func(newValue int) { view.changeTiles(setWallTextureIndexTo(level.AtlasIndex(newValue))) })
|
||||
values.RenderUnifiedSliderInt(readOnly, "Wall Texture Offset", wallTextureOffsetUnifier,
|
||||
func(u values.Unifier) int { return int(u.Unified().(level.TileHeightUnit)) },
|
||||
tileHeightFormatter,
|
||||
0, int(level.TileHeightUnitMax)-1,
|
||||
func(newValue int) { view.changeTiles(setWallTextureOffsetTo(level.TileHeightUnit(newValue))) })
|
||||
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "Use Adjacent Wall Texture", useAdjacentWallTextureUnifier,
|
||||
func(newValue bool) { view.changeTiles(setUseAdjacentWallTextureTo(newValue)) })
|
||||
wallTexturePatterns := level.WallTexturePatterns()
|
||||
values.RenderUnifiedCombo(readOnly, "Wall Texture Pattern", wallTexturePatternUnifier,
|
||||
func(u values.Unifier) int { return int(u.Unified().(level.WallTexturePattern)) },
|
||||
func(value int) string { return wallTexturePatterns[value].String() },
|
||||
len(wallTexturePatterns),
|
||||
func(newValue int) { view.changeTiles(setWallTexturePatternTo(wallTexturePatterns[newValue])) })
|
||||
|
||||
imgui.Separator()
|
||||
|
||||
if imgui.BeginCombo("Shadow View", view.model.shadowDisplay.String()) {
|
||||
displays := ColorDisplays()
|
||||
for _, display := range displays {
|
||||
displayString := display.String()
|
||||
|
||||
if imgui.SelectableV(displayString, display == view.model.shadowDisplay, 0, imgui.Vec2{}) {
|
||||
view.model.shadowDisplay = display
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
|
||||
values.RenderUnifiedSliderInt(readOnly, "Floor Light", floorLightUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string { return "%d" },
|
||||
0, level.GradesOfShadow-1,
|
||||
func(newValue int) { view.changeTiles(setFloorLightTo(newValue)) })
|
||||
values.RenderUnifiedSliderInt(readOnly, "Floor Light Delta", floorLightDeltaUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string { return "%d" },
|
||||
0, level.GradesOfShadow-1,
|
||||
func(newValue int) { view.changeTiles(setFloorLightDeltaTo(newValue)) })
|
||||
values.RenderUnifiedSliderInt(readOnly, "Ceiling Light", ceilingLightUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string { return "%d" },
|
||||
0, level.GradesOfShadow-1,
|
||||
func(newValue int) { view.changeTiles(setCeilingLightTo(newValue)) })
|
||||
values.RenderUnifiedSliderInt(readOnly, "Ceiling Light Delta", ceilingLightDeltaUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string { return "%d" },
|
||||
0, level.GradesOfShadow-1,
|
||||
func(newValue int) { view.changeTiles(setCeilingLightDeltaTo(newValue)) })
|
||||
|
||||
imgui.Separator()
|
||||
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "Deconstructed", deconstructedUnifier,
|
||||
func(newValue bool) { view.changeTiles(setDeconstructedTo(newValue)) })
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "Floor Hazard", floorHazardUnifier,
|
||||
func(newValue bool) { view.changeTiles(setFloorHazardTo(newValue)) })
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "Ceiling Hazard", ceilingHazardUnifier,
|
||||
func(newValue bool) { view.changeTiles(setCeilingHazardTo(newValue)) })
|
||||
}
|
||||
|
||||
imgui.PopItemWidth()
|
||||
}
|
||||
|
||||
func (view *TilesView) renderTextureSelector(readOnly bool, label string, unifier values.Unifier,
|
||||
atlas level.TextureAtlas, minIndex, maxIndex int, changeHandler func(int)) {
|
||||
selectedIndex := -1
|
||||
if unifier.IsUnique() {
|
||||
selectedIndex = unifier.Unified().(int)
|
||||
}
|
||||
|
||||
count := maxIndex - minIndex + 1
|
||||
if count > len(atlas) {
|
||||
count = len(atlas)
|
||||
}
|
||||
render.TextureSelector(label, -1, view.guiScale, count, selectedIndex-minIndex,
|
||||
view.textureCache,
|
||||
func(index int) resource.Key {
|
||||
return resource.KeyOf(ids.LargeTextures.Plus(int(atlas[minIndex+index])), resource.LangAny, 0)
|
||||
},
|
||||
func(index int) string { return view.textureName(int(atlas[minIndex+index])) },
|
||||
func(index int) {
|
||||
if !readOnly {
|
||||
changeHandler(index)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (view *TilesView) textureName(index int) string {
|
||||
key := resource.KeyOf(ids.TextureNames, resource.LangDefault, index)
|
||||
name, err := view.textCache.Text(key)
|
||||
suffix := ""
|
||||
if err == nil {
|
||||
suffix = ": " + name
|
||||
}
|
||||
return fmt.Sprintf("%3d", index) + suffix
|
||||
}
|
||||
|
||||
func setTileTypeTo(tileType level.TileType) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Type = tileType
|
||||
}
|
||||
}
|
||||
|
||||
func setFloorHeightTo(height level.TileHeightUnit) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Floor = tile.Floor.WithAbsoluteHeight(height)
|
||||
}
|
||||
}
|
||||
|
||||
func setCeilingHeightTo(height level.TileHeightUnit) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Ceiling = tile.Ceiling.WithAbsoluteHeight(height)
|
||||
}
|
||||
}
|
||||
|
||||
func setSlopeHeightTo(height level.TileHeightUnit) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.SlopeHeight = height
|
||||
}
|
||||
}
|
||||
|
||||
func setSlopeControlTo(value level.TileSlopeControl) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Flags = tile.Flags.WithSlopeControl(value)
|
||||
}
|
||||
}
|
||||
|
||||
func setPerilTo(value bool) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Flags = tile.Flags.WithPeril(value)
|
||||
}
|
||||
}
|
||||
|
||||
func setMusicZoneTo(value level.TileMusicZone) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Flags = tile.Flags.WithMusicZone(value)
|
||||
}
|
||||
}
|
||||
|
||||
func setFloorTextureIndexTo(value level.AtlasIndex) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.TextureInfo = tile.TextureInfo.WithFloorTextureIndex(value)
|
||||
}
|
||||
}
|
||||
|
||||
func setFloorTextureRotationsTo(value int) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Floor = tile.Floor.WithTextureRotations(value)
|
||||
}
|
||||
}
|
||||
|
||||
func setCeilingTextureIndexTo(value level.AtlasIndex) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.TextureInfo = tile.TextureInfo.WithCeilingTextureIndex(value)
|
||||
}
|
||||
}
|
||||
|
||||
func setCeilingTextureRotationsTo(value int) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Ceiling = tile.Ceiling.WithTextureRotations(value)
|
||||
}
|
||||
}
|
||||
|
||||
func setWallTextureIndexTo(value level.AtlasIndex) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.TextureInfo = tile.TextureInfo.WithWallTextureIndex(value)
|
||||
}
|
||||
}
|
||||
|
||||
func setWallTextureOffsetTo(value level.TileHeightUnit) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Flags = tile.Flags.ForRealWorld().WithWallTextureOffset(value).AsTileFlag()
|
||||
}
|
||||
}
|
||||
|
||||
func setUseAdjacentWallTextureTo(value bool) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Flags = tile.Flags.ForRealWorld().WithUseAdjacentWallTexture(value).AsTileFlag()
|
||||
}
|
||||
}
|
||||
|
||||
func setWallTexturePatternTo(value level.WallTexturePattern) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Flags = tile.Flags.ForRealWorld().WithWallTexturePattern(value).AsTileFlag()
|
||||
}
|
||||
}
|
||||
|
||||
func setFloorLightTo(value int) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Flags = tile.Flags.ForRealWorld().WithFloorShadow(level.GradesOfShadow - 1 - value).AsTileFlag()
|
||||
}
|
||||
}
|
||||
|
||||
func setFloorLightDeltaTo(value int) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.LightDelta = tile.LightDelta.WithFloor(value)
|
||||
}
|
||||
}
|
||||
|
||||
func setCeilingLightTo(value int) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Flags = tile.Flags.ForRealWorld().WithCeilingShadow(level.GradesOfShadow - 1 - value).AsTileFlag()
|
||||
}
|
||||
}
|
||||
|
||||
func setCeilingLightDeltaTo(value int) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.LightDelta = tile.LightDelta.WithCeiling(value)
|
||||
}
|
||||
}
|
||||
|
||||
func setDeconstructedTo(value bool) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Flags = tile.Flags.ForRealWorld().WithDeconstructed(value).AsTileFlag()
|
||||
}
|
||||
}
|
||||
|
||||
func setFloorHazardTo(value bool) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Floor = tile.Floor.WithHazard(value)
|
||||
}
|
||||
}
|
||||
|
||||
func setCeilingHazardTo(value bool) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Ceiling = tile.Ceiling.WithHazard(value)
|
||||
}
|
||||
}
|
||||
|
||||
func setFloorPaletteIndexTo(value int) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.TextureInfo = tile.TextureInfo.WithFloorPaletteIndex(byte(value))
|
||||
}
|
||||
}
|
||||
|
||||
func setCeilingPaletteIndexTo(value int) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.TextureInfo = tile.TextureInfo.WithCeilingPaletteIndex(byte(value))
|
||||
}
|
||||
}
|
||||
|
||||
func setFlightPullTypeTo(value level.CyberspaceFlightPull) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Flags = tile.Flags.ForCyberspace().WithFlightPull(value).AsTileFlag()
|
||||
}
|
||||
}
|
||||
|
||||
func setGameOfLightStateTo(value int) tileMapEntryModifier {
|
||||
return func(tile *level.TileMapEntry) {
|
||||
tile.Flags = tile.Flags.ForCyberspace().WithGameOfLifeState(value).AsTileFlag()
|
||||
}
|
||||
}
|
||||
|
||||
type tileMapEntryModifier func(*level.TileMapEntry)
|
||||
|
||||
func (view *TilesView) changeTiles(modifier tileMapEntryModifier) {
|
||||
err := view.registry.Register(cmd.Named("ChangeTiles"),
|
||||
cmd.Forward(view.restoreFocusTask()),
|
||||
cmd.Nested(func() error { return view.editor.ChangeTiles(modifier) }),
|
||||
cmd.Reverse(view.restoreFocusTask()))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (view *TilesView) restoreFocusTask() cmd.Task {
|
||||
return func(modder world.Modder) error {
|
||||
view.model.restoreFocus = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package levels
|
||||
|
||||
type tilesViewModel struct {
|
||||
textureDisplay TextureDisplay
|
||||
shadowDisplay ColorDisplay
|
||||
cyberColorDisplay ColorDisplay
|
||||
|
||||
restoreFocus bool
|
||||
windowOpen bool
|
||||
}
|
||||
|
||||
func freshTilesViewModel() tilesViewModel {
|
||||
return tilesViewModel{
|
||||
textureDisplay: TextureDisplayFloor,
|
||||
shadowDisplay: ColorDisplayNone,
|
||||
cyberColorDisplay: ColorDisplayNone,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package levels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
||||
)
|
||||
|
||||
type hazardFormatter func(int) string
|
||||
|
||||
func rawHazardValue(int) string {
|
||||
return "raw: %d"
|
||||
}
|
||||
|
||||
func lbpHazardValue(value int) string {
|
||||
return fmt.Sprintf("%3.1f LBP - raw: %%d", float32(value)/2)
|
||||
}
|
||||
|
||||
func percentHazardValue(value int) string {
|
||||
return fmt.Sprintf("%v%%%% - raw: %%d", value*25)
|
||||
}
|
||||
|
||||
type ceilingHazardInfo struct {
|
||||
title string
|
||||
radiationRegister bool
|
||||
formatter hazardFormatter
|
||||
}
|
||||
|
||||
var ceilingHazards = []ceilingHazardInfo{
|
||||
{title: "Off", radiationRegister: false, formatter: rawHazardValue},
|
||||
{title: "Radiation", radiationRegister: true, formatter: lbpHazardValue},
|
||||
}
|
||||
|
||||
func currentCeilingHazard(param *level.Parameters) ceilingHazardInfo {
|
||||
radiationRegister := param.RadiationRegister > 1
|
||||
if radiationRegister {
|
||||
return ceilingHazards[1]
|
||||
}
|
||||
return ceilingHazards[0]
|
||||
}
|
||||
|
||||
type floorHazardInfo struct {
|
||||
title string
|
||||
biohazardRegister bool
|
||||
isGravity bool
|
||||
formatter hazardFormatter
|
||||
}
|
||||
|
||||
var floorHazards = []floorHazardInfo{
|
||||
{title: "Off", biohazardRegister: false, isGravity: false, formatter: rawHazardValue},
|
||||
{title: "Gravity", biohazardRegister: false, isGravity: true, formatter: percentHazardValue},
|
||||
{title: "Biohazard", biohazardRegister: true, isGravity: false, formatter: lbpHazardValue},
|
||||
}
|
||||
|
||||
func currentFloorHazard(param *level.Parameters) floorHazardInfo {
|
||||
biohazardRegister := param.BiohazardRegister > 1
|
||||
isGravity := param.FloorHazardIsGravity != 0
|
||||
if isGravity {
|
||||
return floorHazards[1]
|
||||
} else if biohazardRegister {
|
||||
return floorHazards[2]
|
||||
}
|
||||
return floorHazards[0]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package messages
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
)
|
||||
|
||||
type messageDataEntry struct {
|
||||
oldData [][]byte
|
||||
newData [][]byte
|
||||
}
|
||||
|
||||
type setMessageDataCommand struct {
|
||||
model *viewModel
|
||||
|
||||
key resource.Key
|
||||
showVerboseText bool
|
||||
|
||||
textEntries map[resource.Language]messageDataEntry
|
||||
audioEntries map[resource.Language]messageDataEntry
|
||||
}
|
||||
|
||||
func (cmd setMessageDataCommand) Do(modder world.Modder) error {
|
||||
return cmd.perform(modder, func(entry messageDataEntry) [][]byte { return entry.newData })
|
||||
}
|
||||
|
||||
func (cmd setMessageDataCommand) Undo(modder world.Modder) error {
|
||||
return cmd.perform(modder, func(entry messageDataEntry) [][]byte { return entry.oldData })
|
||||
}
|
||||
|
||||
func (cmd setMessageDataCommand) perform(modder world.Modder, dataResolver func(messageDataEntry) [][]byte) error {
|
||||
cmd.saveEntries(modder, cmd.key.ID.Plus(cmd.key.Index), cmd.textEntries, dataResolver)
|
||||
cmd.saveEntries(modder, cmd.key.ID.Plus(cmd.key.Index).Plus(300), cmd.audioEntries, dataResolver)
|
||||
|
||||
cmd.model.restoreFocus = true
|
||||
cmd.model.currentKey = cmd.key
|
||||
cmd.model.showVerboseText = cmd.showVerboseText
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmd setMessageDataCommand) saveEntries(modder world.Modder, id resource.ID,
|
||||
entries map[resource.Language]messageDataEntry,
|
||||
dataResolver func(messageDataEntry) [][]byte) {
|
||||
for lang, entry := range entries {
|
||||
data := dataResolver(entry)
|
||||
if len(data) > 0 {
|
||||
modder.SetResourceBlocks(lang, id, data)
|
||||
} else {
|
||||
modder.DelResource(lang, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
package messages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/external"
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/editor/values"
|
||||
"github.com/inkyblackness/hacked/ss1/content/audio"
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie"
|
||||
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||
"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"
|
||||
)
|
||||
|
||||
type messageInfo struct {
|
||||
title string
|
||||
}
|
||||
|
||||
var knownMessageTypes = map[resource.ID]messageInfo{
|
||||
ids.MailsStart: {title: "Mails"},
|
||||
ids.LogsStart: {title: "Logs"},
|
||||
ids.FragmentsStart: {title: "Fragments"},
|
||||
}
|
||||
var knownMessageTypesOrder = []resource.ID{ids.MailsStart, ids.LogsStart, ids.FragmentsStart}
|
||||
|
||||
var knownMailTypes = []string{
|
||||
"Audio/Text",
|
||||
"VMail: 'Shield' (0)",
|
||||
"VMail: 'Grove' (1)",
|
||||
"VMail: 'Bridge' (2)",
|
||||
"VMail: 'Laser' (3, 4)",
|
||||
"VMail: 'Status' (11)",
|
||||
"VMail: 'Explode' (5..9)",
|
||||
}
|
||||
|
||||
const videoMailDisplayBase = 256
|
||||
|
||||
// View provides edit controls for messages.
|
||||
type View struct {
|
||||
mod *world.Mod
|
||||
messageCache *text.ElectronicMessageCache
|
||||
cp text.Codepage
|
||||
movieCache *movie.Cache
|
||||
imageCache *graphics.TextureCache
|
||||
|
||||
modalStateMachine gui.ModalStateMachine
|
||||
clipboard external.Clipboard
|
||||
guiScale float32
|
||||
commander cmd.Commander
|
||||
|
||||
model viewModel
|
||||
}
|
||||
|
||||
// NewMessagesView returns a new instance.
|
||||
func NewMessagesView(mod *world.Mod, messageCache *text.ElectronicMessageCache, cp text.Codepage,
|
||||
movieCache *movie.Cache, imageCache *graphics.TextureCache,
|
||||
modalStateMachine gui.ModalStateMachine, clipboard external.Clipboard,
|
||||
guiScale float32, commander cmd.Commander) *View {
|
||||
view := &View{
|
||||
mod: mod,
|
||||
messageCache: messageCache,
|
||||
cp: cp,
|
||||
movieCache: movieCache,
|
||||
imageCache: imageCache,
|
||||
|
||||
modalStateMachine: modalStateMachine,
|
||||
clipboard: clipboard,
|
||||
guiScale: guiScale,
|
||||
commander: commander,
|
||||
|
||||
model: freshViewModel(),
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// WindowOpen returns the flag address, to be used with the main menu.
|
||||
func (view *View) WindowOpen() *bool {
|
||||
return &view.model.windowOpen
|
||||
}
|
||||
|
||||
// Render renders the view.
|
||||
func (view *View) Render() {
|
||||
if view.model.restoreFocus {
|
||||
imgui.SetNextWindowFocus()
|
||||
view.model.restoreFocus = false
|
||||
view.model.windowOpen = true
|
||||
}
|
||||
if view.model.windowOpen {
|
||||
imgui.SetNextWindowSizeV(imgui.Vec2{X: 640 * view.guiScale, Y: 480 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||
if imgui.BeginV("Messages", view.WindowOpen(), imgui.WindowFlagsNoCollapse) {
|
||||
view.renderContent()
|
||||
}
|
||||
imgui.End()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) renderContent() {
|
||||
if imgui.BeginChildV("Properties", imgui.Vec2{X: 350 * view.guiScale, Y: -200 * view.guiScale}, false, 0) {
|
||||
imgui.PushItemWidth(-150 * view.guiScale)
|
||||
if imgui.BeginCombo("Message Type", knownMessageTypes[view.model.currentKey.ID].title) {
|
||||
for _, id := range knownMessageTypesOrder {
|
||||
if imgui.SelectableV(knownMessageTypes[id].title, id == view.model.currentKey.ID, 0, imgui.Vec2{}) {
|
||||
view.model.currentKey.ID = id
|
||||
view.model.currentKey.Index = 0
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
info, _ := ids.Info(view.model.currentKey.ID)
|
||||
index := view.model.currentKey.Index
|
||||
if gui.StepSliderInt("Index", &index, 0, info.MaxCount-1) {
|
||||
view.model.currentKey.Index = index
|
||||
}
|
||||
if imgui.BeginCombo("Language", view.model.currentKey.Lang.String()) {
|
||||
languages := resource.Languages()
|
||||
for _, lang := range languages {
|
||||
if imgui.SelectableV(lang.String(), lang == view.model.currentKey.Lang, 0, imgui.Vec2{}) {
|
||||
view.model.currentKey.Lang = lang
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
|
||||
message, msgReadOnly := view.currentMessage()
|
||||
availableDisplays := view.availableDisplays()
|
||||
|
||||
if imgui.ButtonV("Clear All", imgui.Vec2{}) {
|
||||
view.requestClearAll()
|
||||
}
|
||||
imgui.SameLine()
|
||||
if !msgReadOnly {
|
||||
if imgui.ButtonV("Remove All", imgui.Vec2{}) {
|
||||
view.requestRemoveAll()
|
||||
}
|
||||
} else {
|
||||
imgui.Text("(read-only)")
|
||||
}
|
||||
if view.hasAudio() {
|
||||
imgui.PushID("audio")
|
||||
imgui.Separator()
|
||||
sound, soundReadOnly := view.currentSound()
|
||||
if !sound.Empty() {
|
||||
imgui.LabelText("Audio", fmt.Sprintf("%.2f sec", sound.Duration()))
|
||||
if imgui.Button("Export") {
|
||||
view.requestExportAudio(sound)
|
||||
}
|
||||
if !soundReadOnly {
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Remove") {
|
||||
view.requestRemoveAudio()
|
||||
}
|
||||
}
|
||||
imgui.SameLine()
|
||||
} else {
|
||||
imgui.LabelText("Audio", "(no sound)")
|
||||
}
|
||||
if imgui.Button("Import") {
|
||||
view.requestImportAudio()
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Clear") {
|
||||
view.requestClearAudio()
|
||||
}
|
||||
imgui.PopID()
|
||||
}
|
||||
imgui.Separator()
|
||||
|
||||
textModes := map[bool]string{true: "Verbose", false: "Terse"}
|
||||
if imgui.BeginCombo("Text Mode", textModes[view.model.showVerboseText]) {
|
||||
for _, setting := range []bool{true, false} {
|
||||
if imgui.SelectableV(textModes[setting], view.model.showVerboseText == setting, 0, imgui.Vec2{}) {
|
||||
view.model.showVerboseText = setting
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
view.renderText(false, "Title", message.Title, func(newValue string) {
|
||||
view.requestTextChange(func(msg *text.ElectronicMessage) { msg.Title = text.Blocked(newValue)[0] })
|
||||
})
|
||||
view.renderText(false, "Sender", message.Sender, func(newValue string) {
|
||||
view.requestTextChange(func(msg *text.ElectronicMessage) { msg.Sender = text.Blocked(newValue)[0] })
|
||||
})
|
||||
view.renderText(false, "Subject", message.Subject, func(newValue string) {
|
||||
view.requestTextChange(func(msg *text.ElectronicMessage) { msg.Subject = text.Blocked(newValue)[0] })
|
||||
})
|
||||
nextMessageIndex := values.NewUnifier()
|
||||
nextMessageIndex.Add(message.NextMessage)
|
||||
values.RenderUnifiedSliderInt(false, "Next Message Index", nextMessageIndex,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(int) string { return "%d" },
|
||||
-1, 255,
|
||||
func(newValue int) {
|
||||
view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.NextMessage = newValue })
|
||||
})
|
||||
|
||||
isInterrupt := values.NewUnifier()
|
||||
isInterrupt.Add(message.IsInterrupt)
|
||||
values.RenderUnifiedCheckboxCombo(false, "Is Interrupt", isInterrupt,
|
||||
func(newValue bool) {
|
||||
view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.IsInterrupt = newValue })
|
||||
})
|
||||
|
||||
colorIndex := values.NewUnifier()
|
||||
colorIndex.Add(message.ColorIndex)
|
||||
values.RenderUnifiedSliderInt(false, "Color Index", colorIndex,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(int) string { return "%d" },
|
||||
-1, 255,
|
||||
func(newValue int) {
|
||||
view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.ColorIndex = newValue })
|
||||
})
|
||||
|
||||
if view.model.currentKey.ID == ids.MailsStart {
|
||||
mailType := message.LeftDisplay - videoMailDisplayBase + 1
|
||||
selectedType := fmt.Sprintf("Unknown (0x%03X)", message.LeftDisplay)
|
||||
if mailType < 0 {
|
||||
mailType = 0
|
||||
}
|
||||
if mailType < len(knownMailTypes) {
|
||||
selectedType = knownMailTypes[mailType]
|
||||
}
|
||||
if msgReadOnly {
|
||||
imgui.LabelText("Mail Type", selectedType)
|
||||
} else if imgui.BeginCombo("Mail Type", selectedType) {
|
||||
for typeIndex, typeString := range knownMailTypes {
|
||||
if imgui.SelectableV(typeString, mailType == typeIndex, 0, imgui.Vec2{}) {
|
||||
newIndex := -1
|
||||
if typeIndex > 0 {
|
||||
newIndex = videoMailDisplayBase + (typeIndex - 1)
|
||||
}
|
||||
view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.LeftDisplay = newIndex })
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
}
|
||||
if message.LeftDisplay < videoMailDisplayBase {
|
||||
view.renderDisplayGallery(false, "Left Display", message.LeftDisplay, availableDisplays,
|
||||
func(newValue int) {
|
||||
view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.LeftDisplay = newValue })
|
||||
})
|
||||
view.renderDisplayGallery(false, "Right Display", message.RightDisplay, availableDisplays,
|
||||
func(newValue int) {
|
||||
view.requestPropertyChange(func(msg *text.ElectronicMessage) { msg.RightDisplay = newValue })
|
||||
})
|
||||
}
|
||||
|
||||
imgui.PopItemWidth()
|
||||
}
|
||||
imgui.EndChild()
|
||||
|
||||
view.renderMFDs()
|
||||
}
|
||||
|
||||
func (view *View) renderMFDs() {
|
||||
message, _ := view.currentMessage()
|
||||
|
||||
view.renderSideImage("LeftMFD", message.LeftDisplay)
|
||||
imgui.SameLineV(0, 0)
|
||||
textToDisplay := message.VerboseText
|
||||
if imgui.BeginChildV("Text", imgui.Vec2{X: -250 * view.guiScale, Y: 190 * view.guiScale}, true, 0) {
|
||||
if !view.model.showVerboseText {
|
||||
textToDisplay = message.TerseText
|
||||
}
|
||||
|
||||
imgui.PushTextWrapPos()
|
||||
if len(textToDisplay) == 0 {
|
||||
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1.0, Y: 1.0, Z: 1.0, W: 0.5})
|
||||
imgui.Text("(empty)")
|
||||
imgui.PopStyleColor()
|
||||
} else {
|
||||
imgui.Text(textToDisplay)
|
||||
}
|
||||
imgui.PopTextWrapPos()
|
||||
}
|
||||
imgui.EndChild()
|
||||
view.clipboardPopup(false, "Text", textToDisplay, func(newValue string) {
|
||||
view.requestTextChange(func(msg *text.ElectronicMessage) {
|
||||
if view.model.showVerboseText {
|
||||
msg.VerboseText = newValue
|
||||
} else {
|
||||
msg.TerseText = newValue
|
||||
}
|
||||
})
|
||||
})
|
||||
imgui.SameLineV(0, 0)
|
||||
view.renderSideImage("RightMFD", message.RightDisplay)
|
||||
}
|
||||
|
||||
func (view View) currentMessage() (msg text.ElectronicMessage, readOnly bool) {
|
||||
msg = view.messageOf(view.model.currentKey)
|
||||
readOnly = len(view.mod.ModifiedBlocks(view.model.currentKey.Lang, view.model.currentKey.ID.Plus(view.model.currentKey.Index))) == 0
|
||||
return
|
||||
}
|
||||
|
||||
func (view View) hasAudio() bool {
|
||||
return (view.model.currentKey.ID == ids.MailsStart) || (view.model.currentKey.ID == ids.LogsStart)
|
||||
}
|
||||
|
||||
func (view *View) currentSound() (audio.L8, bool) {
|
||||
if view.hasAudio() {
|
||||
key := view.model.currentKey
|
||||
key.ID = key.ID.Plus(300)
|
||||
sound, _ := view.movieCache.Audio(key)
|
||||
readOnly := len(view.mod.ModifiedBlocks(key.Lang, key.ID.Plus(view.model.currentKey.Index))) == 0
|
||||
return sound, readOnly
|
||||
}
|
||||
return audio.L8{}, true
|
||||
}
|
||||
|
||||
func (view View) messageOf(key resource.Key) text.ElectronicMessage {
|
||||
msg, cacheErr := view.messageCache.Message(key)
|
||||
if cacheErr != nil {
|
||||
msg = text.EmptyElectronicMessage()
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (view *View) availableDisplays() int {
|
||||
selector := view.mod.LocalizedResources(view.model.currentKey.Lang)
|
||||
res, err := selector.Select(ids.MfdDataBitmaps)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return res.BlockCount()
|
||||
}
|
||||
|
||||
func (view *View) renderText(readOnly bool, label string, value string, changeCallback func(string)) {
|
||||
imgui.LabelText(label, value)
|
||||
view.clipboardPopup(readOnly, label, value, changeCallback)
|
||||
}
|
||||
|
||||
func (view *View) clipboardPopup(readOnly bool, label string, value string, changeCallback func(string)) {
|
||||
if imgui.BeginPopupContextItemV(label+"-Popup", 1) {
|
||||
if imgui.Selectable("Copy to Clipboard") {
|
||||
view.clipboard.SetString(value)
|
||||
}
|
||||
if !readOnly && imgui.Selectable("Copy from Clipboard") {
|
||||
newValue, err := view.clipboard.String()
|
||||
if err == nil {
|
||||
changeCallback(newValue)
|
||||
}
|
||||
}
|
||||
imgui.EndPopup()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) renderDisplayGallery(readOnly bool, label string, index, availableDisplays int,
|
||||
changeCallback func(int)) {
|
||||
unifier := values.NewUnifier()
|
||||
unifier.Add(index)
|
||||
values.RenderUnifiedSliderInt(readOnly, label, unifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(int) string { return "%d" },
|
||||
-1, availableDisplays-1,
|
||||
changeCallback)
|
||||
render.TextureSelector("###"+label+" Texture", -1, view.guiScale, availableDisplays,
|
||||
index, view.imageCache,
|
||||
func(index int) resource.Key {
|
||||
return resource.KeyOf(ids.MfdDataBitmaps, view.model.currentKey.Lang, index)
|
||||
},
|
||||
func(index int) string { return fmt.Sprintf("%d", index) },
|
||||
changeCallback)
|
||||
}
|
||||
|
||||
func (view *View) renderSideImage(label string, index int) {
|
||||
size := imgui.Vec2{X: 250 * view.guiScale, Y: 190 * view.guiScale}
|
||||
imgui.PushStyleVarVec2(imgui.StyleVarWindowPadding, imgui.Vec2{X: size.X, Y: -size.Y})
|
||||
if imgui.BeginChildV(label, size, false, 0) {
|
||||
if index >= 0 {
|
||||
key := resource.KeyOf(ids.MfdDataBitmaps, view.model.currentKey.Lang, index)
|
||||
render.TextureImage(label, view.imageCache, key, size)
|
||||
}
|
||||
}
|
||||
imgui.PopStyleVar()
|
||||
imgui.EndChild()
|
||||
}
|
||||
|
||||
func (view *View) requestExportAudio(sound audio.L8) {
|
||||
filename := fmt.Sprintf("%05d_%s.wav",
|
||||
view.model.currentKey.ID.Plus(view.model.currentKey.Index).Plus(300).Value(),
|
||||
view.model.currentKey.Lang.String())
|
||||
|
||||
external.ExportAudio(view.modalStateMachine, filename, sound)
|
||||
}
|
||||
|
||||
func (view *View) requestRemoveAudio() {
|
||||
view.requestAudioChange(nil)
|
||||
}
|
||||
|
||||
func (view *View) requestImportAudio() {
|
||||
external.ImportAudio(view.modalStateMachine, func(sound audio.L8) {
|
||||
movieData := movie.ContainSoundData(sound)
|
||||
view.requestAudioChange(movieData)
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) requestClearAudio() {
|
||||
view.requestAudioChange(view.silence())
|
||||
}
|
||||
|
||||
func (view View) silence() []byte {
|
||||
silence := audio.L8{SampleRate: 22050, Samples: []byte{0x80}}
|
||||
return movie.ContainSoundData(silence)
|
||||
}
|
||||
|
||||
func (view *View) requestClearAll() {
|
||||
view.requestWipe(text.EmptyElectronicMessage().Encode(view.cp), view.silence())
|
||||
}
|
||||
|
||||
func (view *View) requestRemoveAll() {
|
||||
view.requestWipe(nil, nil)
|
||||
}
|
||||
|
||||
func (view *View) requestWipe(newTextData [][]byte, newAudioData []byte) {
|
||||
textEntries := make(map[resource.Language]messageDataEntry)
|
||||
audioEntries := make(map[resource.Language]messageDataEntry)
|
||||
textID := view.model.currentKey.ID.Plus(view.model.currentKey.Index)
|
||||
for lang := resource.Language(0); lang < resource.LanguageCount; lang++ {
|
||||
textEntries[lang] = messageDataEntry{
|
||||
oldData: view.mod.ModifiedBlocks(lang, textID),
|
||||
newData: newTextData,
|
||||
}
|
||||
if view.hasAudio() {
|
||||
audioEntries[lang] = messageDataEntry{
|
||||
oldData: view.mod.ModifiedBlocks(lang, textID.Plus(300)),
|
||||
newData: [][]byte{newAudioData},
|
||||
}
|
||||
}
|
||||
}
|
||||
view.requestSetMessageData(textEntries, audioEntries)
|
||||
}
|
||||
|
||||
func (view *View) requestTextChange(modifier func(*text.ElectronicMessage)) {
|
||||
msg := view.messageOf(view.model.currentKey)
|
||||
entries := make(map[resource.Language]messageDataEntry)
|
||||
modifier(&msg)
|
||||
|
||||
entries[view.model.currentKey.Lang] = messageDataEntry{
|
||||
oldData: view.mod.ModifiedBlocks(view.model.currentKey.Lang, view.model.currentKey.ID.Plus(view.model.currentKey.Index)),
|
||||
newData: msg.Encode(view.cp),
|
||||
}
|
||||
view.requestSetMessageData(entries, nil)
|
||||
}
|
||||
|
||||
func (view *View) requestPropertyChange(modifier func(*text.ElectronicMessage)) {
|
||||
entries := make(map[resource.Language]messageDataEntry)
|
||||
for lang := resource.Language(0); lang < resource.LanguageCount; lang++ {
|
||||
key := view.model.currentKey
|
||||
key.Lang = lang
|
||||
msg := view.messageOf(key)
|
||||
modifier(&msg)
|
||||
|
||||
entries[lang] = messageDataEntry{
|
||||
oldData: view.mod.ModifiedBlocks(lang, key.ID.Plus(key.Index)),
|
||||
newData: msg.Encode(view.cp),
|
||||
}
|
||||
}
|
||||
view.requestSetMessageData(entries, nil)
|
||||
}
|
||||
|
||||
func (view *View) requestAudioChange(movieData []byte) {
|
||||
entries := make(map[resource.Language]messageDataEntry)
|
||||
|
||||
entries[view.model.currentKey.Lang] = messageDataEntry{
|
||||
oldData: view.mod.ModifiedBlocks(view.model.currentKey.Lang, view.model.currentKey.ID.Plus(view.model.currentKey.Index).Plus(300)),
|
||||
newData: [][]byte{movieData},
|
||||
}
|
||||
view.requestSetMessageData(nil, entries)
|
||||
}
|
||||
|
||||
func (view *View) requestSetMessageData(textEntries map[resource.Language]messageDataEntry, audioEntries map[resource.Language]messageDataEntry) {
|
||||
command := setMessageDataCommand{
|
||||
key: view.model.currentKey,
|
||||
showVerboseText: view.model.showVerboseText,
|
||||
model: &view.model,
|
||||
textEntries: textEntries,
|
||||
audioEntries: audioEntries,
|
||||
}
|
||||
view.commander.Queue(command)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package messages
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
type viewModel struct {
|
||||
windowOpen bool
|
||||
restoreFocus bool
|
||||
|
||||
currentKey resource.Key
|
||||
showVerboseText bool
|
||||
}
|
||||
|
||||
func freshViewModel() viewModel {
|
||||
return viewModel{
|
||||
currentKey: resource.KeyOf(ids.MailsStart, resource.LangDefault, 0),
|
||||
showVerboseText: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package movies
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie"
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
type compressionListenerFunc func(compressionResult)
|
||||
|
||||
type compressingStartState struct {
|
||||
machine gui.ModalStateMachine
|
||||
view *View
|
||||
|
||||
width int
|
||||
height int
|
||||
input movie.Scene
|
||||
listener compressionListenerFunc
|
||||
}
|
||||
|
||||
func (state compressingStartState) Render() {
|
||||
imgui.OpenPopup("Compressing...")
|
||||
task := newCompressionTask(state.input, state.width, state.height)
|
||||
state.machine.SetState(&compressingWaitingState{
|
||||
machine: state.machine,
|
||||
view: state.view,
|
||||
input: state.input,
|
||||
listener: state.listener,
|
||||
task: task,
|
||||
})
|
||||
go task.run()
|
||||
}
|
||||
|
||||
func (state compressingStartState) HandleFiles(names []string) {
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package movies
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie"
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
type compressingWaitingState struct {
|
||||
machine gui.ModalStateMachine
|
||||
view *View
|
||||
|
||||
input movie.Scene
|
||||
listener compressionListenerFunc
|
||||
task *compressionTask
|
||||
}
|
||||
|
||||
func (state *compressingWaitingState) Render() {
|
||||
if imgui.BeginPopupModalV("Compressing...", nil,
|
||||
imgui.WindowFlagsNoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
|
||||
imgui.Text("This may take a while.\n" +
|
||||
"Yes, your PC is probably capable of recoding HD movies way quicker;\n" +
|
||||
"Sadly, this codec of '94 is quite tricky.")
|
||||
|
||||
if imgui.Button("Cancel") {
|
||||
state.task.cancel()
|
||||
}
|
||||
|
||||
result := state.task.update()
|
||||
if result != nil {
|
||||
state.machine.SetState(nil)
|
||||
imgui.CloseCurrentPopup()
|
||||
|
||||
state.listener(result)
|
||||
}
|
||||
|
||||
imgui.EndPopup()
|
||||
} else {
|
||||
state.machine.SetState(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (state *compressingWaitingState) HandleFiles(names []string) {
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package movies
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie"
|
||||
)
|
||||
|
||||
type compressionTask struct {
|
||||
width int
|
||||
height int
|
||||
input movie.Scene
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
resultChan chan compressionResult
|
||||
}
|
||||
|
||||
type compressionResult interface{}
|
||||
|
||||
type compressionAborted struct{}
|
||||
|
||||
type compressionFailed struct{ err error }
|
||||
|
||||
type compressionFinished struct{ scene movie.HighResScene }
|
||||
|
||||
func newCompressionTask(scene movie.Scene, width, height int) *compressionTask {
|
||||
task := &compressionTask{
|
||||
width: width,
|
||||
height: height,
|
||||
input: scene,
|
||||
resultChan: make(chan compressionResult),
|
||||
}
|
||||
task.ctx, task.ctxCancel = context.WithCancel(context.Background())
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
func (task *compressionTask) run() {
|
||||
defer close(task.resultChan)
|
||||
highResScene, err := movie.HighResSceneFrom(task.ctx, task.input, task.width, task.height)
|
||||
switch {
|
||||
case task.ctx.Err() != nil:
|
||||
task.resultChan <- compressionAborted{}
|
||||
case err != nil:
|
||||
task.resultChan <- compressionFailed{err: err}
|
||||
default:
|
||||
task.resultChan <- compressionFinished{scene: highResScene}
|
||||
}
|
||||
}
|
||||
|
||||
func (task *compressionTask) update() compressionResult {
|
||||
select {
|
||||
case result, ok := <-task.resultChan:
|
||||
if ok {
|
||||
return result
|
||||
}
|
||||
return compressionAborted{}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (task *compressionTask) cancel() {
|
||||
task.ctxCancel()
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
package movies
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/gif"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/asticode/go-astisub"
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/external"
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/ss1/content/audio"
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie"
|
||||
"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"
|
||||
)
|
||||
|
||||
type movieInfo struct {
|
||||
title string
|
||||
multilang bool
|
||||
}
|
||||
|
||||
var knownMovies = map[resource.ID]movieInfo{
|
||||
ids.MovieIntro: {title: "Intro", multilang: true},
|
||||
ids.MovieDeath: {title: "Death", multilang: false},
|
||||
ids.MovieEnd: {title: "End", multilang: false},
|
||||
}
|
||||
|
||||
var knownMoviesOrder = []resource.ID{ids.MovieIntro, ids.MovieDeath, ids.MovieEnd}
|
||||
|
||||
// View provides edit controls for animations.
|
||||
type View struct {
|
||||
mod *world.Mod
|
||||
|
||||
frameCache *graphics.FrameCache
|
||||
frameCacheKey graphics.FrameCacheKey
|
||||
|
||||
movieService undoable.MovieService
|
||||
|
||||
modalStateMachine gui.ModalStateMachine
|
||||
guiScale float32
|
||||
commander cmd.Commander
|
||||
|
||||
model viewModel
|
||||
}
|
||||
|
||||
// NewMoviesView returns a new instance.
|
||||
func NewMoviesView(mod *world.Mod, frameCache *graphics.FrameCache,
|
||||
movieService undoable.MovieService,
|
||||
modalStateMachine gui.ModalStateMachine, guiScale float32, commander cmd.Commander) *View {
|
||||
view := &View{
|
||||
mod: mod,
|
||||
|
||||
frameCache: frameCache,
|
||||
frameCacheKey: frameCache.AllocateKey(),
|
||||
|
||||
movieService: movieService,
|
||||
|
||||
modalStateMachine: modalStateMachine,
|
||||
guiScale: guiScale,
|
||||
commander: commander,
|
||||
|
||||
model: freshViewModel(),
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// WindowOpen returns the flag address, to be used with the main menu.
|
||||
func (view *View) WindowOpen() *bool {
|
||||
return &view.model.windowOpen
|
||||
}
|
||||
|
||||
// Render renders the view.
|
||||
func (view *View) Render() {
|
||||
if view.model.restoreFocus {
|
||||
imgui.SetNextWindowFocus()
|
||||
view.model.restoreFocus = false
|
||||
view.model.windowOpen = true
|
||||
}
|
||||
if view.model.windowOpen {
|
||||
imgui.SetNextWindowSizeV(imgui.Vec2{X: 800 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||
if imgui.BeginV("Movies", view.WindowOpen(), imgui.WindowFlagsNoCollapse|imgui.WindowFlagsHorizontalScrollbar) {
|
||||
view.renderContent()
|
||||
}
|
||||
imgui.End()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) renderContent() {
|
||||
if imgui.BeginChildV("Properties", imgui.Vec2{X: 350 * view.guiScale, Y: 0}, false, 0) {
|
||||
imgui.PushItemWidth(-150 * view.guiScale)
|
||||
if imgui.BeginCombo("Movie", knownMovies[view.model.currentKey.ID].title) {
|
||||
for _, id := range knownMoviesOrder {
|
||||
if imgui.SelectableV(knownMovies[id].title, id == view.model.currentKey.ID, 0, imgui.Vec2{}) {
|
||||
view.model.currentKey.ID = id
|
||||
view.model.currentKey.Index = 0
|
||||
view.model.currentScene = 0
|
||||
view.model.currentFrame = 0
|
||||
view.model.frameTimeFraction = -1
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
|
||||
if knownMovies[view.model.currentKey.ID].multilang {
|
||||
if imgui.BeginCombo("Language", view.model.currentKey.Lang.String()) {
|
||||
languages := resource.Languages()
|
||||
for _, lang := range languages {
|
||||
if imgui.SelectableV(lang.String(), lang == view.model.currentKey.Lang, 0, imgui.Vec2{}) {
|
||||
view.model.currentKey.Lang = lang
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
} else {
|
||||
imgui.LabelText("Language", "(not localized)")
|
||||
}
|
||||
|
||||
imgui.Separator()
|
||||
|
||||
view.renderProperties()
|
||||
|
||||
imgui.PopItemWidth()
|
||||
}
|
||||
imgui.EndChild()
|
||||
|
||||
scenes := view.movieService.Video(view.model.currentKey)
|
||||
imgui.SameLine()
|
||||
imgui.BeginGroup()
|
||||
imgui.BeginChildV("Scenes", imgui.Vec2{X: 200 * view.guiScale, Y: -60 * view.guiScale}, true,
|
||||
imgui.WindowFlagsHorizontalScrollbar|imgui.WindowFlagsAlwaysVerticalScrollbar)
|
||||
for index, scene := range scenes {
|
||||
sceneInfo := fmt.Sprintf("Scene %02d (%d frames)", index, len(scene.Frames))
|
||||
if imgui.SelectableV(sceneInfo, view.model.currentScene == index, 0, imgui.Vec2{}) {
|
||||
view.model.currentScene = index
|
||||
view.model.currentFrame = 0
|
||||
view.model.frameTimeFraction = -1
|
||||
}
|
||||
}
|
||||
imgui.EndChild()
|
||||
if imgui.Button("Up") {
|
||||
view.requestMoveSceneEarlier()
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Down") {
|
||||
view.requestMoveSceneLater()
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Remove") {
|
||||
view.requestRemoveScene()
|
||||
}
|
||||
if imgui.Button("Import") {
|
||||
view.requestImportScene("")
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Export") {
|
||||
view.requestExportScene()
|
||||
}
|
||||
imgui.EndGroup()
|
||||
imgui.SameLine()
|
||||
if imgui.BeginChildV("Frames", imgui.Vec2{X: -1, Y: 0}, false, 0) {
|
||||
var scene *movie.Scene
|
||||
if (view.model.currentScene >= 0) && (view.model.currentScene < len(scenes)) {
|
||||
scene = &scenes[view.model.currentScene]
|
||||
}
|
||||
if scene != nil {
|
||||
var frame *movie.Frame
|
||||
if (view.model.currentFrame >= 0) && (view.model.currentFrame < len(scene.Frames)) {
|
||||
frame = &scene.Frames[view.model.currentFrame]
|
||||
}
|
||||
if frame != nil {
|
||||
// This code updates the texture every render cycle. In case of performance loss, this is a point to optimize.
|
||||
view.frameCache.SetTexture(view.frameCacheKey, 600, 300, frame.Pixels, &scene.Palette)
|
||||
|
||||
render.FrameImage("Frame", view.frameCache, view.frameCacheKey,
|
||||
imgui.Vec2{
|
||||
X: float32(movie.HighResDefaultWidth) * view.guiScale,
|
||||
Y: float32(movie.HighResDefaultHeight) * view.guiScale,
|
||||
})
|
||||
}
|
||||
|
||||
imgui.PushItemWidth(-150 * view.guiScale)
|
||||
gui.StepSliderInt("Frame Index", &view.model.currentFrame, 0, len(scene.Frames)-1)
|
||||
imgui.PopItemWidth()
|
||||
if (frame != nil) && (frame.DisplayTime < (time.Second / 4)) {
|
||||
imgui.Separator()
|
||||
if view.model.frameTimeFraction < 0 {
|
||||
view.model.frameTimeFraction = int((frame.DisplayTime * 0x10000) / time.Second)
|
||||
}
|
||||
imgui.PushItemWidth(-150 * view.guiScale)
|
||||
gui.StepSliderInt("Frame time fraction", &view.model.frameTimeFraction, 0, 0x3FFF)
|
||||
imgui.PopItemWidth()
|
||||
if imgui.Button("Set for scene") {
|
||||
displayTime := (time.Duration(view.model.frameTimeFraction) * time.Second) / 0x10000
|
||||
view.requestSetFramesDisplayTime(displayTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
imgui.EndChild()
|
||||
}
|
||||
|
||||
func (view *View) renderProperties() {
|
||||
view.renderAudioProperties()
|
||||
view.renderSubtitlesProperties()
|
||||
|
||||
if view.movieService.SizeWarning(view.model.currentKey) {
|
||||
view.renderSizeWarning()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) renderAudioProperties() {
|
||||
imgui.PushID("audio")
|
||||
imgui.Separator()
|
||||
sound := view.currentSound()
|
||||
imgui.LabelText("Audio", fmt.Sprintf("%.2f sec", sound.Duration()))
|
||||
if imgui.Button("Import") {
|
||||
view.requestImportAudio()
|
||||
}
|
||||
if !sound.Empty() {
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Export") {
|
||||
view.requestExportAudio(sound)
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Clear") {
|
||||
view.requestClearAudio()
|
||||
}
|
||||
}
|
||||
imgui.PopID()
|
||||
}
|
||||
|
||||
func (view *View) renderSubtitlesProperties() {
|
||||
imgui.PushID("subtitles")
|
||||
imgui.Separator()
|
||||
if imgui.BeginCombo("Sub Language", view.model.currentSubtitleLang.String()) {
|
||||
languages := resource.Languages()
|
||||
for _, lang := range languages {
|
||||
if imgui.SelectableV(lang.String(), lang == view.model.currentSubtitleLang, 0, imgui.Vec2{}) {
|
||||
view.model.currentSubtitleLang = lang
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
sub := view.currentSubtitles()
|
||||
imgui.Text(fmt.Sprintf("%d lines", len(sub.Entries)))
|
||||
if imgui.Button("Import") {
|
||||
view.requestImportSubtitles()
|
||||
}
|
||||
if len(sub.Entries) > 0 {
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Export") {
|
||||
view.requestExportSubtitles()
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Clear") {
|
||||
view.requestClearSubtitles()
|
||||
}
|
||||
}
|
||||
imgui.PopID()
|
||||
}
|
||||
|
||||
func (view *View) renderSizeWarning() {
|
||||
imgui.Separator()
|
||||
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1, Y: 0, Z: 0, W: 1})
|
||||
imgui.Text("Size warning - reduce content!")
|
||||
imgui.PopStyleColor()
|
||||
if imgui.IsItemHovered() {
|
||||
imgui.BeginTooltip()
|
||||
imgui.SetTooltip("The movie has reached a size larger than officially supported.\n" +
|
||||
"While the game might still show the movie, a future update may stop supporting it.")
|
||||
imgui.EndTooltip()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) restoreFunc() func() {
|
||||
return view.restoreFuncWithScene(view.model.currentScene)
|
||||
}
|
||||
|
||||
func (view *View) restoreFuncWithScene(oldScene int) func() {
|
||||
oldKey := view.model.currentKey
|
||||
oldSubtitlesLang := view.model.currentSubtitleLang
|
||||
oldFrame := view.model.currentFrame
|
||||
|
||||
return func() {
|
||||
view.model.restoreFocus = true
|
||||
view.model.currentKey = oldKey
|
||||
view.model.currentSubtitleLang = oldSubtitlesLang
|
||||
view.model.currentScene = oldScene
|
||||
view.model.currentFrame = oldFrame
|
||||
view.model.frameTimeFraction = -1
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) currentSound() audio.L8 {
|
||||
return view.movieService.Audio(view.model.currentKey)
|
||||
}
|
||||
|
||||
func (view *View) requestExportAudio(sound audio.L8) {
|
||||
filename := fmt.Sprintf("%s_%s.wav", knownMovies[view.model.currentKey.ID].title, view.model.currentKey.Lang.String())
|
||||
|
||||
external.ExportAudio(view.modalStateMachine, filename, sound)
|
||||
}
|
||||
|
||||
func (view *View) requestImportAudio() {
|
||||
external.ImportAudio(view.modalStateMachine, func(sound audio.L8) {
|
||||
view.movieService.RequestSetAudio(view.model.currentKey, sound, view.restoreFunc())
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) requestClearAudio() {
|
||||
view.movieService.RequestSetAudio(view.model.currentKey, audio.L8{}, view.restoreFunc())
|
||||
}
|
||||
|
||||
func (view *View) currentSubtitles() movie.SubtitleList {
|
||||
return view.movieService.Subtitles(view.model.currentKey, view.model.currentSubtitleLang)
|
||||
}
|
||||
|
||||
func (view View) requestExportSubtitles() {
|
||||
filename := fmt.Sprintf("%s_%s.srt", knownMovies[view.model.currentKey.ID].title, view.model.currentSubtitleLang.String())
|
||||
info := "File to be written: " + filename
|
||||
var exportTo func(string)
|
||||
currentSubtitles := view.currentSubtitles()
|
||||
|
||||
exportTo = func(dirname string) {
|
||||
writer, err := os.Create(filepath.Join(dirname, filename))
|
||||
if err != nil {
|
||||
external.Export(view.modalStateMachine, "Could not create file.\n"+info, exportTo, true)
|
||||
return
|
||||
}
|
||||
defer func() { _ = writer.Close() }()
|
||||
|
||||
sub := astisub.NewSubtitles()
|
||||
|
||||
var lastItem *astisub.Item
|
||||
for _, entry := range currentSubtitles.Entries {
|
||||
var item astisub.Item
|
||||
var line astisub.Line
|
||||
line.Items = append(line.Items, astisub.LineItem{Text: entry.Text})
|
||||
item.Lines = []astisub.Line{line}
|
||||
item.StartAt = entry.Timestamp
|
||||
item.EndAt = item.StartAt
|
||||
if lastItem != nil {
|
||||
lastItem.EndAt = item.StartAt
|
||||
}
|
||||
lastItem = &item
|
||||
sub.Items = append(sub.Items, lastItem)
|
||||
}
|
||||
|
||||
err = sub.WriteToSRT(writer)
|
||||
if err != nil {
|
||||
external.Export(view.modalStateMachine, "Could not export subtitles.\n"+info, exportTo, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
external.Export(view.modalStateMachine, info, exportTo, false)
|
||||
}
|
||||
|
||||
func (view *View) requestImportSubtitles() {
|
||||
info := "File must be an .SRT file."
|
||||
types := []external.TypeInfo{{Title: "Subtitle files (*.srt)", Extensions: []string{"srt"}}}
|
||||
var fileHandler func(string)
|
||||
|
||||
fileHandler = func(filename string) {
|
||||
reader, err := os.Open(filename)
|
||||
if err != nil {
|
||||
external.Import(view.modalStateMachine, "Could not open file.\n"+info, types, fileHandler, true)
|
||||
return
|
||||
}
|
||||
defer func() { _ = reader.Close() }()
|
||||
|
||||
subtitles, err := astisub.ReadFromSRT(reader)
|
||||
if err != nil {
|
||||
external.Import(view.modalStateMachine, "File not recognized as SRT.\n"+info, types, fileHandler, true)
|
||||
return
|
||||
}
|
||||
var newSubtitles movie.SubtitleList
|
||||
for _, item := range subtitles.Items {
|
||||
var newEntry movie.Subtitle
|
||||
newEntry.Timestamp = item.StartAt
|
||||
for _, line := range item.Lines {
|
||||
for _, lineItem := range line.Items {
|
||||
if len(newEntry.Text) > 0 {
|
||||
newEntry.Text += "\n"
|
||||
}
|
||||
newEntry.Text += lineItem.Text
|
||||
}
|
||||
}
|
||||
newSubtitles.Entries = append(newSubtitles.Entries, newEntry)
|
||||
}
|
||||
|
||||
view.movieService.RequestSetSubtitles(view.model.currentKey, view.model.currentSubtitleLang,
|
||||
newSubtitles, view.restoreFunc())
|
||||
}
|
||||
|
||||
external.Import(view.modalStateMachine, info, types, fileHandler, false)
|
||||
}
|
||||
|
||||
func (view *View) requestClearSubtitles() {
|
||||
view.movieService.RequestSetSubtitles(view.model.currentKey, view.model.currentSubtitleLang,
|
||||
movie.SubtitleList{}, view.restoreFunc())
|
||||
}
|
||||
|
||||
func (view *View) requestImportScene(returningInfo string) {
|
||||
info := fmt.Sprintf("File must be an animated GIF file in size %dx%d.",
|
||||
movie.HighResDefaultWidth, movie.HighResDefaultHeight)
|
||||
types := []external.TypeInfo{{Title: "Animation files (*.gif)", Extensions: []string{"gif"}}}
|
||||
var fileHandler func(string)
|
||||
|
||||
fileHandler = func(filename string) {
|
||||
reader, err := os.Open(filename)
|
||||
if err != nil {
|
||||
external.Import(view.modalStateMachine, "Could not open file.\n"+info, types, fileHandler, true)
|
||||
return
|
||||
}
|
||||
defer func() { _ = reader.Close() }()
|
||||
data, err := gif.DecodeAll(reader)
|
||||
if err != nil {
|
||||
external.Import(view.modalStateMachine, "File not recognized as GIF.\n"+info, types, fileHandler, true)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.Config.Width != movie.HighResDefaultWidth) || (data.Config.Height != movie.HighResDefaultHeight) {
|
||||
external.Import(view.modalStateMachine, info, types, fileHandler, true)
|
||||
return
|
||||
}
|
||||
|
||||
var scene movie.Scene
|
||||
scene.Frames = make([]movie.Frame, len(data.Image))
|
||||
var palette bitmap.Palette
|
||||
if gifPalette, isPal := data.Config.ColorModel.(color.Palette); isPal {
|
||||
for index, clr := range gifPalette {
|
||||
r, g, b, _ := clr.RGBA()
|
||||
palette[index].Red = byte(r >> 8)
|
||||
palette[index].Green = byte(g >> 8)
|
||||
palette[index].Blue = byte(b >> 8)
|
||||
}
|
||||
}
|
||||
framebuffer := make([]byte, data.Config.Width*data.Config.Height)
|
||||
framebufferSnapshot := func() []byte {
|
||||
buf := make([]byte, len(framebuffer))
|
||||
copy(buf, framebuffer)
|
||||
return buf
|
||||
}
|
||||
|
||||
var usedColors [256]bool
|
||||
for _, img := range data.Image {
|
||||
for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
|
||||
for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
|
||||
colorIndex := img.ColorIndexAt(x, y)
|
||||
usedColors[colorIndex] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
var indexMap [256]byte
|
||||
for i := 0; i < 256; i++ {
|
||||
indexMap[byte(i)] = byte(i)
|
||||
}
|
||||
// Ensure color index 0 is not used.
|
||||
if usedColors[0] && (data.BackgroundIndex != 0) {
|
||||
if data.BackgroundIndex == 255 {
|
||||
indexMap[0] = palette.IndexClosestTo(palette[0], []byte{0, 255})
|
||||
} else {
|
||||
palette[0], palette[data.BackgroundIndex] = palette[data.BackgroundIndex], palette[0]
|
||||
indexMap[0] = data.BackgroundIndex
|
||||
indexMap[data.BackgroundIndex] = 0
|
||||
}
|
||||
}
|
||||
palette[0] = bitmap.RGB{Red: 0x00, Green: 0x00, Blue: 0x00} // default color for background
|
||||
// In case the last index is in use by the animation, use a similar color as this entry is reserved for subtitles.
|
||||
if usedColors[255] && (data.BackgroundIndex != 255) {
|
||||
indexMap[255] = palette.IndexClosestTo(palette[255], []byte{0, 255})
|
||||
}
|
||||
palette[255] = bitmap.RGB{Red: 0x9A, Green: 0x35, Blue: 0x35} // default color in intro
|
||||
|
||||
scene.Palette = palette
|
||||
for index, img := range data.Image {
|
||||
for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ {
|
||||
for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ {
|
||||
colorIndex := img.ColorIndexAt(x, y)
|
||||
if colorIndex != data.BackgroundIndex {
|
||||
framebuffer[y*data.Config.Width+x] = indexMap[colorIndex]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scene.Frames[index].DisplayTime = time.Duration(data.Delay[index]) * 10 * time.Millisecond // nolint: durationcheck
|
||||
scene.Frames[index].Pixels = framebufferSnapshot()
|
||||
}
|
||||
|
||||
view.compressAndAddScene(scene, data.Config.Width, data.Config.Height)
|
||||
}
|
||||
|
||||
external.Import(view.modalStateMachine, returningInfo+info, types, fileHandler, false)
|
||||
}
|
||||
|
||||
func (view *View) compressAndAddScene(scene movie.Scene, width, height int) {
|
||||
view.modalStateMachine.SetState(&compressingStartState{
|
||||
machine: view.modalStateMachine,
|
||||
view: view,
|
||||
width: width,
|
||||
height: height,
|
||||
input: scene,
|
||||
listener: view.onCompressionResult,
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) onCompressionResult(result compressionResult) {
|
||||
switch typedResult := result.(type) {
|
||||
case compressionAborted:
|
||||
case compressionFinished:
|
||||
view.requestAddScene(typedResult.scene)
|
||||
case compressionFailed:
|
||||
view.requestImportScene("Could not compress. Follow recommendations and retry.\n" +
|
||||
"Technical details:\n" + typedResult.err.Error() + "\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) requestAddScene(scene movie.HighResScene) {
|
||||
view.movieService.RequestAddScene(view.model.currentKey, scene, view.restoreFunc())
|
||||
}
|
||||
|
||||
func (view *View) requestExportScene() {
|
||||
filename := fmt.Sprintf("%s_Scene%02d_%s.gif",
|
||||
knownMovies[view.model.currentKey.ID].title,
|
||||
view.model.currentScene,
|
||||
view.model.currentKey.Lang.String())
|
||||
info := "File to be written: " + filename
|
||||
var exportTo func(string)
|
||||
|
||||
scenes := view.movieService.Video(view.model.currentKey)
|
||||
var scene *movie.Scene
|
||||
if view.model.currentScene >= 0 && view.model.currentScene < len(scenes) {
|
||||
scene = &scenes[view.model.currentScene]
|
||||
}
|
||||
|
||||
if (scene == nil) || len(scene.Frames) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
exportTo = func(dirname string) {
|
||||
writer, err := os.Create(filepath.Join(dirname, filename))
|
||||
if err != nil {
|
||||
external.Export(view.modalStateMachine, "Could not create file.\n"+info, exportTo, true)
|
||||
return
|
||||
}
|
||||
defer func() { _ = writer.Close() }()
|
||||
|
||||
colorPalette := scene.Palette.ColorPalette(false)
|
||||
data := gif.GIF{
|
||||
Config: image.Config{
|
||||
Width: 600,
|
||||
Height: 300,
|
||||
ColorModel: colorPalette,
|
||||
},
|
||||
LoopCount: -1,
|
||||
}
|
||||
|
||||
imageRect := image.Rect(0, 0, data.Config.Width, data.Config.Height)
|
||||
for _, frame := range scene.Frames {
|
||||
frameImg := image.NewPaletted(imageRect, colorPalette)
|
||||
frameImg.Pix = frame.Pixels
|
||||
frameImg.Stride = data.Config.Width
|
||||
data.Image = append(data.Image, frameImg)
|
||||
data.Delay = append(data.Delay, int(((frame.DisplayTime/time.Millisecond)+5)/10))
|
||||
}
|
||||
|
||||
err = gif.EncodeAll(writer, &data)
|
||||
if err != nil {
|
||||
external.Export(view.modalStateMachine, info, exportTo, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
external.Export(view.modalStateMachine, info, exportTo, false)
|
||||
}
|
||||
|
||||
func (view *View) requestMoveSceneEarlier() {
|
||||
scenes := view.movieService.Video(view.model.currentKey)
|
||||
if (view.model.currentScene > 0) && (view.model.currentScene < len(scenes)) {
|
||||
view.movieService.RequestMoveSceneEarlier(view.model.currentKey, view.model.currentScene,
|
||||
view.restoreFuncWithScene(view.model.currentScene-1))
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) requestMoveSceneLater() {
|
||||
scenes := view.movieService.Video(view.model.currentKey)
|
||||
if (view.model.currentScene >= 0) && (view.model.currentScene < (len(scenes) - 1)) {
|
||||
view.movieService.RequestMoveSceneLater(view.model.currentKey, view.model.currentScene,
|
||||
view.restoreFuncWithScene(view.model.currentScene+1))
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) requestRemoveScene() {
|
||||
scenes := view.movieService.Video(view.model.currentKey)
|
||||
if (view.model.currentScene >= 0) && (view.model.currentScene < len(scenes)) {
|
||||
view.movieService.RequestRemoveScene(view.model.currentKey, view.model.currentScene, view.restoreFunc())
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) requestSetFramesDisplayTime(displayTime time.Duration) {
|
||||
view.movieService.RequestSetSceneFramesDisplayTime(view.model.currentKey, view.model.currentScene, displayTime, view.restoreFunc())
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package movies
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
type viewModel struct {
|
||||
windowOpen bool
|
||||
restoreFocus bool
|
||||
|
||||
currentKey resource.Key
|
||||
currentSubtitleLang resource.Language
|
||||
currentScene int
|
||||
currentFrame int
|
||||
|
||||
frameTimeFraction int
|
||||
}
|
||||
|
||||
func freshViewModel() viewModel {
|
||||
return viewModel{
|
||||
currentKey: resource.KeyOf(ids.MovieIntro, resource.LangDefault, 0),
|
||||
currentSubtitleLang: resource.LangDefault,
|
||||
frameTimeFraction: -1,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
)
|
||||
|
||||
type setObjectBitmapCommand struct {
|
||||
model *viewModel
|
||||
|
||||
triple object.Triple
|
||||
bitmap int
|
||||
|
||||
resourceKey resource.Key
|
||||
oldData []byte
|
||||
newData []byte
|
||||
}
|
||||
|
||||
func (command setObjectBitmapCommand) Do(modder world.Modder) error {
|
||||
return command.perform(modder, command.newData)
|
||||
}
|
||||
|
||||
func (command setObjectBitmapCommand) Undo(modder world.Modder) error {
|
||||
return command.perform(modder, command.oldData)
|
||||
}
|
||||
|
||||
func (command setObjectBitmapCommand) perform(modder world.Modder, data []byte) error {
|
||||
modder.SetResourceBlock(command.resourceKey.Lang, command.resourceKey.ID, command.resourceKey.Index, data)
|
||||
|
||||
command.model.restoreFocus = true
|
||||
command.model.currentObject = command.triple
|
||||
command.model.currentBitmap = command.bitmap
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
)
|
||||
|
||||
type setObjectPropertiesCommand struct {
|
||||
model *viewModel
|
||||
|
||||
triple object.Triple
|
||||
bitmap int
|
||||
|
||||
oldProperties object.Properties
|
||||
newProperties object.Properties
|
||||
}
|
||||
|
||||
func (command setObjectPropertiesCommand) Do(modder world.Modder) error {
|
||||
return command.perform(modder, command.newProperties)
|
||||
}
|
||||
|
||||
func (command setObjectPropertiesCommand) Undo(modder world.Modder) error {
|
||||
return command.perform(modder, command.oldProperties)
|
||||
}
|
||||
|
||||
func (command setObjectPropertiesCommand) perform(modder world.Modder, properties object.Properties) error {
|
||||
modder.SetObjectProperties(command.triple, properties)
|
||||
|
||||
command.model.restoreFocus = true
|
||||
command.model.currentObject = command.triple
|
||||
command.model.currentBitmap = command.bitmap
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
)
|
||||
|
||||
type setObjectTextCommand struct {
|
||||
model *viewModel
|
||||
|
||||
triple object.Triple
|
||||
bitmap int
|
||||
key resource.Key
|
||||
|
||||
oldData []byte
|
||||
newData []byte
|
||||
}
|
||||
|
||||
func (command setObjectTextCommand) Do(modder world.Modder) error {
|
||||
return command.perform(modder, command.newData)
|
||||
}
|
||||
|
||||
func (command setObjectTextCommand) Undo(modder world.Modder) error {
|
||||
return command.perform(modder, command.oldData)
|
||||
}
|
||||
|
||||
func (command setObjectTextCommand) perform(modder world.Modder, data []byte) error {
|
||||
modder.SetResourceBlock(command.key.Lang, command.key.ID, command.key.Index, data)
|
||||
command.model.restoreFocus = true
|
||||
command.model.currentObject = command.triple
|
||||
command.model.currentBitmap = command.bitmap
|
||||
command.model.currentLang = command.key.Lang
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/external"
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/editor/values"
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object/objprop"
|
||||
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||
"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"
|
||||
)
|
||||
|
||||
// View provides edit controls for game objects.
|
||||
type View struct {
|
||||
mod *world.Mod
|
||||
textCache *text.Cache
|
||||
cp text.Codepage
|
||||
imageCache *graphics.TextureCache
|
||||
paletteCache *graphics.PaletteCache
|
||||
|
||||
modalStateMachine gui.ModalStateMachine
|
||||
clipboard external.Clipboard
|
||||
guiScale float32
|
||||
commander cmd.Commander
|
||||
|
||||
model viewModel
|
||||
}
|
||||
|
||||
// NewView returns a new instance.
|
||||
func NewView(mod *world.Mod, textCache *text.Cache, cp text.Codepage,
|
||||
imageCache *graphics.TextureCache, paletteCache *graphics.PaletteCache,
|
||||
modalStateMachine gui.ModalStateMachine,
|
||||
clipboard external.Clipboard, guiScale float32, commander cmd.Commander) *View {
|
||||
view := &View{
|
||||
mod: mod,
|
||||
textCache: textCache,
|
||||
cp: cp,
|
||||
imageCache: imageCache,
|
||||
paletteCache: paletteCache,
|
||||
|
||||
modalStateMachine: modalStateMachine,
|
||||
clipboard: clipboard,
|
||||
guiScale: guiScale,
|
||||
commander: commander,
|
||||
|
||||
model: freshViewModel(),
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// WindowOpen returns the flag address, to be used with the main menu.
|
||||
func (view *View) WindowOpen() *bool {
|
||||
return &view.model.windowOpen
|
||||
}
|
||||
|
||||
// Render renders the view.
|
||||
func (view *View) Render() {
|
||||
if view.model.restoreFocus {
|
||||
imgui.SetNextWindowFocus()
|
||||
view.model.restoreFocus = false
|
||||
view.model.windowOpen = true
|
||||
}
|
||||
if view.model.windowOpen {
|
||||
imgui.SetNextWindowSizeV(imgui.Vec2{X: 800 * view.guiScale, Y: 600 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||
if imgui.BeginV("Game Objects", view.WindowOpen(), imgui.WindowFlagsNoCollapse|imgui.WindowFlagsHorizontalScrollbar) {
|
||||
view.renderContent()
|
||||
}
|
||||
imgui.End()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) renderContent() {
|
||||
if imgui.BeginChildV("Properties", imgui.Vec2{X: -330 * view.guiScale, Y: 0}, false, imgui.WindowFlagsHorizontalScrollbar) {
|
||||
imgui.PushItemWidth(-260 * view.guiScale)
|
||||
classString := func(class object.Class) string {
|
||||
return fmt.Sprintf("%2d: %v", int(class), class)
|
||||
}
|
||||
if imgui.BeginCombo("Object Class", classString(view.model.currentObject.Class)) {
|
||||
for _, class := range object.Classes() {
|
||||
if imgui.SelectableV(classString(class), class == view.model.currentObject.Class, 0, imgui.Vec2{}) {
|
||||
view.model.currentObject = object.TripleFrom(int(class), 0, 0)
|
||||
view.model.currentBitmap = 0
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
if imgui.BeginCombo("Object Type", view.tripleName(view.model.currentObject)) {
|
||||
allTypes := view.mod.ObjectProperties().TriplesInClass(view.model.currentObject.Class)
|
||||
for _, triple := range allTypes {
|
||||
if imgui.SelectableV(view.tripleName(triple), triple == view.model.currentObject, 0, imgui.Vec2{}) {
|
||||
view.model.currentObject = triple
|
||||
view.model.currentBitmap = 0
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
|
||||
readOnly := !view.mod.HasModifiableObjectProperties()
|
||||
properties, propErr := view.mod.ObjectProperties().ForObject(view.model.currentObject)
|
||||
|
||||
imgui.Separator()
|
||||
bitmapLimit := 0
|
||||
if propErr == nil {
|
||||
bitmapLimit = int(properties.Common.Bitmap3D.FrameNumber() + 2)
|
||||
}
|
||||
gui.StepSliderInt("Bitmap", &view.model.currentBitmap, 0, bitmapLimit)
|
||||
render.TextureSelector("BitmapSelector", -1, view.guiScale, bitmapLimit+1, view.model.currentBitmap,
|
||||
view.imageCache, view.currentBitmapKeyFor, func(int) string { return "" }, func(newValue int) {
|
||||
view.model.currentBitmap = newValue
|
||||
})
|
||||
|
||||
imgui.Separator()
|
||||
|
||||
if imgui.BeginCombo("Language", view.model.currentLang.String()) {
|
||||
languages := resource.Languages()
|
||||
for _, lang := range languages {
|
||||
if imgui.SelectableV(lang.String(), lang == view.model.currentLang, 0, imgui.Vec2{}) {
|
||||
view.model.currentLang = lang
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
view.renderText(false, "Long Name",
|
||||
view.objectName(view.model.currentObject, view.model.currentLang, true),
|
||||
func(newValue string) {
|
||||
view.requestSetObjectName(view.model.currentObject, true, newValue)
|
||||
})
|
||||
view.renderText(false, "Short Name",
|
||||
view.objectName(view.model.currentObject, view.model.currentLang, false),
|
||||
func(newValue string) {
|
||||
view.requestSetObjectName(view.model.currentObject, false, newValue)
|
||||
})
|
||||
|
||||
if propErr == nil {
|
||||
if imgui.TreeNodeV("Common Properties", imgui.TreeNodeFlagsDefaultOpen|imgui.TreeNodeFlagsFramed) {
|
||||
view.renderCommonProperties(readOnly, properties)
|
||||
imgui.TreePop()
|
||||
}
|
||||
if imgui.TreeNodeV("Generic Properties", imgui.TreeNodeFlagsFramed) {
|
||||
view.renderGenericProperties(readOnly, properties)
|
||||
imgui.TreePop()
|
||||
}
|
||||
if imgui.TreeNodeV("Specific Properties", imgui.TreeNodeFlagsFramed) {
|
||||
view.renderSpecificProperties(readOnly, properties)
|
||||
imgui.TreePop()
|
||||
}
|
||||
} else {
|
||||
imgui.Text("(properties unavailable)")
|
||||
}
|
||||
|
||||
imgui.PopItemWidth()
|
||||
}
|
||||
imgui.EndChild()
|
||||
imgui.SameLine()
|
||||
|
||||
imgui.BeginGroup()
|
||||
view.renderObjectBitmap()
|
||||
imgui.EndGroup()
|
||||
}
|
||||
|
||||
func (view *View) renderText(readOnly bool, label string, value string, changeCallback func(string)) {
|
||||
imgui.LabelText(label, value)
|
||||
view.clipboardPopup(readOnly, label, value, changeCallback)
|
||||
}
|
||||
|
||||
func (view *View) tripleName(triple object.Triple) string {
|
||||
return triple.String() + ": " + view.objectName(triple, resource.LangDefault, true)
|
||||
}
|
||||
|
||||
func (view *View) objectName(triple object.Triple, lang resource.Language, longName bool) string {
|
||||
result := "???"
|
||||
linearIndex := view.mod.ObjectProperties().TripleIndex(triple)
|
||||
if linearIndex >= 0 {
|
||||
nameID := ids.ObjectShortNames
|
||||
if longName {
|
||||
nameID = ids.ObjectLongNames
|
||||
}
|
||||
key := resource.KeyOf(nameID, lang, linearIndex)
|
||||
objName, err := view.textCache.Text(key)
|
||||
if err == nil {
|
||||
result = objName
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (view *View) clipboardPopup(readOnly bool, label string, value string, changeCallback func(string)) {
|
||||
if imgui.BeginPopupContextItemV(label+"-Popup", 1) {
|
||||
if imgui.Selectable("Copy to Clipboard") {
|
||||
view.clipboard.SetString(value)
|
||||
}
|
||||
if !readOnly && imgui.Selectable("Copy from Clipboard") {
|
||||
newValue, err := view.clipboard.String()
|
||||
if err == nil {
|
||||
changeCallback(newValue)
|
||||
}
|
||||
}
|
||||
imgui.EndPopup()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) requestSetObjectName(triple object.Triple, longName bool, newValue string) {
|
||||
linearIndex := view.mod.ObjectProperties().TripleIndex(triple)
|
||||
if linearIndex >= 0 {
|
||||
id := ids.ObjectShortNames
|
||||
if longName {
|
||||
id = ids.ObjectLongNames
|
||||
}
|
||||
key := resource.KeyOf(id, view.model.currentLang, linearIndex)
|
||||
oldValue, _ := view.textCache.Text(key)
|
||||
|
||||
if oldValue != newValue {
|
||||
command := setObjectTextCommand{
|
||||
model: &view.model,
|
||||
triple: view.model.currentObject,
|
||||
bitmap: view.model.currentBitmap,
|
||||
key: key,
|
||||
oldData: view.cp.Encode(oldValue),
|
||||
newData: view.cp.Encode(text.Blocked(newValue)[0]),
|
||||
}
|
||||
view.commander.Queue(command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) requestSetObjectProperties(modifier func(*object.Properties)) {
|
||||
command := setObjectPropertiesCommand{
|
||||
model: &view.model,
|
||||
triple: view.model.currentObject,
|
||||
bitmap: view.model.currentBitmap,
|
||||
}
|
||||
currentProp, err := view.mod.ObjectProperties().ForObject(command.triple)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
command.oldProperties = currentProp.Clone()
|
||||
command.newProperties = currentProp.Clone()
|
||||
modifier(&command.newProperties)
|
||||
view.commander.Queue(command)
|
||||
}
|
||||
|
||||
func (view *View) renderCommonProperties(readOnly bool, properties *object.Properties) {
|
||||
intIdentity := func(u values.Unifier) int { return u.Unified().(int) }
|
||||
intFormat := func(value int) string { return "%d" }
|
||||
|
||||
if imgui.TreeNode("Flags") {
|
||||
renderFlag := func(flag object.CommonFlag) {
|
||||
flagUnifier := values.NewUnifier()
|
||||
flagUnifier.Add(properties.Common.Flags.Has(flag))
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, flag.String(), flagUnifier, func(newValue bool) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
if newValue {
|
||||
prop.Common.Flags = prop.Common.Flags.With(flag)
|
||||
} else {
|
||||
prop.Common.Flags = prop.Common.Flags.Without(flag)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
for _, flag := range object.CommonFlags() {
|
||||
renderFlag(flag)
|
||||
}
|
||||
|
||||
imgui.TreePop()
|
||||
}
|
||||
|
||||
lightTypes := object.LightTypes()
|
||||
lightTypeUnifier := values.NewUnifier()
|
||||
lightTypeUnifier.Add(int(properties.Common.Flags.LightType()))
|
||||
values.RenderUnifiedCombo(readOnly, "LightType", lightTypeUnifier, intIdentity,
|
||||
func(value int) string {
|
||||
return object.LightType(value).String()
|
||||
}, len(lightTypes), func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.Flags = prop.Common.Flags.WithLightType(object.LightType(newValue))
|
||||
})
|
||||
})
|
||||
|
||||
useModeUnifier := values.NewUnifier()
|
||||
useModeUnifier.Add(int(properties.Common.Flags.UseMode()))
|
||||
values.RenderUnifiedCombo(readOnly, "UseMode", useModeUnifier, intIdentity,
|
||||
func(value int) string {
|
||||
return object.UseMode(value).String()
|
||||
}, 4, func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.Flags = prop.Common.Flags.WithUseMode(object.UseMode(newValue))
|
||||
})
|
||||
})
|
||||
|
||||
massUnifier := values.NewUnifier()
|
||||
massUnifier.Add(int(properties.Common.Mass))
|
||||
values.RenderUnifiedSliderInt(readOnly, "Mass", massUnifier, intIdentity, intFormat, -1, 5000,
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.Mass = int32(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
hitpointsUnifier := values.NewUnifier()
|
||||
hitpointsUnifier.Add(int(properties.Common.Hitpoints))
|
||||
values.RenderUnifiedSliderInt(readOnly, "Hitpoints", hitpointsUnifier, intIdentity, intFormat, 0, 10000,
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.Hitpoints = int16(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
armorUnifier := values.NewUnifier()
|
||||
armorUnifier.Add(int(properties.Common.Armor))
|
||||
values.RenderUnifiedSliderInt(readOnly, "Armor", armorUnifier, intIdentity, intFormat, 0, 255,
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.Armor = byte(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
renderTypeUnifier := values.NewUnifier()
|
||||
renderTypeUnifier.Add(int(properties.Common.RenderType))
|
||||
values.RenderUnifiedCombo(readOnly, "Render Type", renderTypeUnifier, intIdentity,
|
||||
func(value int) string { return object.RenderType(value).String() },
|
||||
len(object.RenderTypes()), func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.RenderType = object.RenderType(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
physicsModelUnifier := values.NewUnifier()
|
||||
physicsModelUnifier.Add(int(properties.Common.PhysicsModel))
|
||||
values.RenderUnifiedCombo(readOnly, "Physics Model", physicsModelUnifier, intIdentity,
|
||||
func(value int) string { return object.PhysicsModel(value).String() },
|
||||
len(object.PhysicsModels()), func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.PhysicsModel = object.PhysicsModel(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
hardnessUnifier := values.NewUnifier()
|
||||
hardnessUnifier.Add(int(properties.Common.Hardness))
|
||||
values.RenderUnifiedSliderInt(readOnly, "Hardness", hardnessUnifier, intIdentity, intFormat, 0, object.HardnessLimit,
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.Hardness = byte(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
physicsXRUnifier := values.NewUnifier()
|
||||
physicsXRUnifier.Add(int(properties.Common.PhysicsXR))
|
||||
values.RenderUnifiedSliderInt(readOnly, "Physics XR", physicsXRUnifier, intIdentity, intFormat, 0, object.PhysicsXRLimit,
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.PhysicsXR = byte(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
physicsZUnifier := values.NewUnifier()
|
||||
physicsZUnifier.Add(int(properties.Common.PhysicsZ))
|
||||
values.RenderUnifiedSliderInt(readOnly, "Physics Z", physicsZUnifier, intIdentity, intFormat, 0, 255,
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.PhysicsZ = byte(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
if imgui.TreeNode("Vulnerabilities") {
|
||||
renderVulnerability := func(damageType object.DamageType) {
|
||||
damageUnifier := values.NewUnifier()
|
||||
damageUnifier.Add(properties.Common.Vulnerabilities.Has(damageType))
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, damageType.String(), damageUnifier, func(newValue bool) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
if newValue {
|
||||
prop.Common.Vulnerabilities = prop.Common.Vulnerabilities.With(damageType)
|
||||
} else {
|
||||
prop.Common.Vulnerabilities = prop.Common.Vulnerabilities.Without(damageType)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
for _, damageType := range object.DamageTypes() {
|
||||
renderVulnerability(damageType)
|
||||
}
|
||||
|
||||
primaryUnifier := values.NewUnifier()
|
||||
primaryUnifier.Add(properties.Common.SpecialVulnerabilities.PrimaryValue())
|
||||
values.RenderUnifiedSliderInt(readOnly, "Primary (double dmg)", primaryUnifier, intIdentity, intFormat, 0, object.SpecialDamageTypeLimit,
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.SpecialVulnerabilities = prop.Common.SpecialVulnerabilities.WithPrimaryValue(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
superUnifier := values.NewUnifier()
|
||||
superUnifier.Add(properties.Common.SpecialVulnerabilities.SuperValue())
|
||||
values.RenderUnifiedSliderInt(readOnly, "Super (quad dmg)", superUnifier, intIdentity, intFormat, 0, object.SpecialDamageTypeLimit,
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.SpecialVulnerabilities = prop.Common.SpecialVulnerabilities.WithSuperValue(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
imgui.TreePop()
|
||||
}
|
||||
|
||||
defenseUnifier := values.NewUnifier()
|
||||
defenseUnifier.Add(int(properties.Common.Defense))
|
||||
values.RenderUnifiedSliderInt(readOnly, "Defense", defenseUnifier, intIdentity,
|
||||
func(value int) string {
|
||||
if value == object.DefenseNoCriticals {
|
||||
return "%d - (no criticals)"
|
||||
}
|
||||
return "%d"
|
||||
}, 0, 255,
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.Defense = byte(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
toughnessUnifier := values.NewUnifier()
|
||||
toughnessUnifier.Add(int(properties.Common.Toughness))
|
||||
values.RenderUnifiedSliderInt(readOnly, "Toughness", toughnessUnifier, intIdentity,
|
||||
func(value int) string {
|
||||
if value == object.ToughnessNoDamage {
|
||||
return "(no damage) -- raw: %d"
|
||||
}
|
||||
return fmt.Sprintf("%d:1 dmg -- raw: %%d", 1<<uint(value))
|
||||
},
|
||||
0, 7,
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.Toughness = byte(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
mfdOrMeshIDUnifier := values.NewUnifier()
|
||||
mfdOrMeshIDUnifier.Add(int(properties.Common.MfdOrMeshID))
|
||||
values.RenderUnifiedSliderInt(readOnly, "MFD/Mesh ID", mfdOrMeshIDUnifier, intIdentity, intFormat, 0, 1000,
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.MfdOrMeshID = uint16(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
bitmap3DBitmapNumUnifier := values.NewUnifier()
|
||||
bitmap3DBitmapNumUnifier.Add(int(properties.Common.Bitmap3D.BitmapNumber()))
|
||||
values.RenderUnifiedSliderInt(readOnly, "Bitmap Number", bitmap3DBitmapNumUnifier, intIdentity, intFormat, 0, int(object.Bitmap3DBitmapNumberLimit),
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.Bitmap3D = prop.Common.Bitmap3D.WithBitmapNumber(uint16(newValue))
|
||||
})
|
||||
})
|
||||
bitmap3DFrameNumUnifier := values.NewUnifier()
|
||||
bitmap3DFrameNumUnifier.Add(int(properties.Common.Bitmap3D.FrameNumber()))
|
||||
values.RenderUnifiedSliderInt(readOnly, "Frame Number", bitmap3DFrameNumUnifier, intIdentity, intFormat, 0, int(object.Bitmap3DFrameNumberLimit),
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.Bitmap3D = prop.Common.Bitmap3D.WithFrameNumber(uint16(newValue))
|
||||
})
|
||||
})
|
||||
bitmap3DAnimUnifier := values.NewUnifier()
|
||||
bitmap3DAnimUnifier.Add(properties.Common.Bitmap3D.Animation())
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "Animation", bitmap3DAnimUnifier,
|
||||
func(newValue bool) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.Bitmap3D = prop.Common.Bitmap3D.WithAnimation(newValue)
|
||||
})
|
||||
})
|
||||
bitmap3DRepeatUnifier := values.NewUnifier()
|
||||
bitmap3DRepeatUnifier.Add(properties.Common.Bitmap3D.Repeat())
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "Repeat", bitmap3DRepeatUnifier,
|
||||
func(newValue bool) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.Bitmap3D = prop.Common.Bitmap3D.WithRepeat(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
destroyEffectValueUnifier := values.NewUnifier()
|
||||
destroyEffectValueUnifier.Add(int(properties.Common.DestroyEffect.Value()))
|
||||
values.RenderUnifiedSliderInt(readOnly, "DestroyEffect Value", destroyEffectValueUnifier,
|
||||
intIdentity, intFormat, 0, int(object.DestroyEffectValueLimit),
|
||||
func(newValue int) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.DestroyEffect = prop.Common.DestroyEffect.WithValue(byte(newValue))
|
||||
})
|
||||
})
|
||||
destroyEffectPlaySoundUnifier := values.NewUnifier()
|
||||
destroyEffectPlaySoundUnifier.Add(properties.Common.DestroyEffect.PlaySound())
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "DestroyEffect PlaySound", destroyEffectPlaySoundUnifier,
|
||||
func(newValue bool) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.DestroyEffect = prop.Common.DestroyEffect.WithSound(newValue)
|
||||
})
|
||||
})
|
||||
destroyEffectShowExplosionUnifier := values.NewUnifier()
|
||||
destroyEffectShowExplosionUnifier.Add(properties.Common.DestroyEffect.ShowExplosion())
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "DestroyEffect ShowExplosion", destroyEffectShowExplosionUnifier,
|
||||
func(newValue bool) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
prop.Common.DestroyEffect = prop.Common.DestroyEffect.WithExplosion(newValue)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) renderGenericProperties(readOnly bool, properties *object.Properties) {
|
||||
readInterpreter := objprop.GenericProperties(view.model.currentObject.Class, properties.Generic)
|
||||
view.createPropertyControls(readOnly, readInterpreter, func(key string, modifier func(uint32) uint32) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
writeInterpreter := objprop.GenericProperties(view.model.currentObject.Class, prop.Generic)
|
||||
view.setInterpreterValueKeyed(writeInterpreter, key, modifier)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) renderSpecificProperties(readOnly bool, properties *object.Properties) {
|
||||
readInterpreter := objprop.SpecificProperties(view.model.currentObject, properties.Specific)
|
||||
view.createPropertyControls(readOnly, readInterpreter, func(key string, modifier func(uint32) uint32) {
|
||||
view.requestSetObjectProperties(func(prop *object.Properties) {
|
||||
writeInterpreter := objprop.SpecificProperties(view.model.currentObject, prop.Specific)
|
||||
view.setInterpreterValueKeyed(writeInterpreter, key, modifier)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) createPropertyControls(readOnly bool, rootInterpreter *interpreters.Instance,
|
||||
updater func(string, func(uint32) uint32)) {
|
||||
objTypeRenderer := values.ObjectTypeControlRenderer{
|
||||
Meta: view.mod.ObjectProperties(),
|
||||
TextCache: view.textCache,
|
||||
}
|
||||
|
||||
var processInterpreter func(string, *interpreters.Instance)
|
||||
processInterpreter = func(path string, interpreter *interpreters.Instance) {
|
||||
for _, key := range interpreter.Keys() {
|
||||
fullKey := path + key
|
||||
unifier := values.NewUnifier()
|
||||
unifier.Add(int32(interpreter.Get(key)))
|
||||
simplifier := values.StandardSimplifier(readOnly, fullKey, unifier,
|
||||
func(modifier func(uint32) uint32) {
|
||||
updater(fullKey, modifier)
|
||||
}, objTypeRenderer)
|
||||
|
||||
interpreter.Describe(key, simplifier)
|
||||
}
|
||||
|
||||
for _, key := range interpreter.ActiveRefinements() {
|
||||
fullKey := path + key
|
||||
if len(fullKey) > 0 {
|
||||
imgui.Separator()
|
||||
imgui.Text(fullKey + ":")
|
||||
}
|
||||
processInterpreter(fullKey+".", interpreter.Refined(key))
|
||||
}
|
||||
}
|
||||
processInterpreter("", rootInterpreter)
|
||||
}
|
||||
|
||||
func (view *View) setInterpreterValueKeyed(instance *interpreters.Instance, key string, modifier func(uint32) uint32) {
|
||||
resolvedInterpreter := instance
|
||||
keys := strings.Split(key, ".")
|
||||
keyCount := len(keys)
|
||||
if len(keys) > 1 {
|
||||
for _, subKey := range keys[:keyCount-1] {
|
||||
resolvedInterpreter = resolvedInterpreter.Refined(subKey)
|
||||
}
|
||||
}
|
||||
resolvedInterpreter.Set(keys[keyCount-1], modifier(resolvedInterpreter.Get(keys[keyCount-1])))
|
||||
}
|
||||
|
||||
func (view *View) renderObjectBitmap() {
|
||||
render.TextureImage("BitmapImage", view.imageCache, view.currentBitmapKey(), imgui.Vec2{X: 320 * view.guiScale, Y: 240 * view.guiScale})
|
||||
if imgui.Button("Clear") {
|
||||
view.requestClearBitmap()
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Import") {
|
||||
view.requestImportBitmap()
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Export") {
|
||||
view.requestExportBitmap()
|
||||
}
|
||||
if view.hasModCurrentBitmap() {
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Remove") {
|
||||
view.requestSetBitmapData(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) requestClearBitmap() {
|
||||
bmp := bitmap.Bitmap{
|
||||
Header: bitmap.Header{
|
||||
Width: 1,
|
||||
Height: 1,
|
||||
},
|
||||
Pixels: []byte{0x00},
|
||||
}
|
||||
view.requestSetBitmap(bmp)
|
||||
}
|
||||
|
||||
func (view *View) requestImportBitmap() {
|
||||
paletteRetriever := func() (bitmap.Palette, error) {
|
||||
palette, err := view.paletteCache.Palette(0)
|
||||
if err != nil {
|
||||
return bitmap.Palette{}, err
|
||||
}
|
||||
return palette.Palette(), nil
|
||||
}
|
||||
external.ImportImage(view.modalStateMachine, paletteRetriever, func(bmp bitmap.Bitmap) {
|
||||
view.requestSetBitmap(bmp)
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) requestExportBitmap() {
|
||||
key := view.currentBitmapKey()
|
||||
texture, err := view.imageCache.Texture(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
palette, err := view.paletteCache.Palette(0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rawPalette := palette.Palette()
|
||||
filename := fmt.Sprintf("%02d_%d_%02d-%02d.png",
|
||||
view.model.currentObject.Class, view.model.currentObject.Subclass, view.model.currentObject.Type, view.model.currentBitmap)
|
||||
width, height := texture.Size()
|
||||
bmp := bitmap.Bitmap{
|
||||
Header: bitmap.Header{
|
||||
Width: int16(width),
|
||||
Height: int16(height),
|
||||
},
|
||||
Pixels: texture.PixelData(),
|
||||
Palette: &rawPalette,
|
||||
}
|
||||
|
||||
external.ExportImage(view.modalStateMachine, filename, bmp)
|
||||
}
|
||||
|
||||
func (view *View) hasModCurrentBitmap() bool {
|
||||
key := view.currentBitmapKey()
|
||||
return len(view.mod.ModifiedBlock(key.Lang, key.ID, key.Index)) > 0
|
||||
}
|
||||
|
||||
func (view *View) requestSetBitmap(bmp bitmap.Bitmap) {
|
||||
highestBitShift := func(value int16) (result byte) {
|
||||
if value != 0 {
|
||||
for (value >> result) != 1 {
|
||||
result++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
bmp.Header.Flags = bitmap.FlagTransparent
|
||||
bmp.Header.Type = bitmap.TypeFlat8Bit
|
||||
bmp.Header.WidthFactor = highestBitShift(bmp.Header.Width)
|
||||
bmp.Header.HeightFactor = highestBitShift(bmp.Header.Height)
|
||||
bmp.Header.Stride = uint16(bmp.Header.Width)
|
||||
data := bitmap.Encode(&bmp, 0)
|
||||
view.requestSetBitmapData(data)
|
||||
}
|
||||
|
||||
func (view *View) requestSetBitmapData(newData []byte) {
|
||||
resourceKey := view.currentBitmapKey()
|
||||
|
||||
command := setObjectBitmapCommand{
|
||||
model: &view.model,
|
||||
triple: view.model.currentObject,
|
||||
bitmap: view.model.currentBitmap,
|
||||
|
||||
resourceKey: resourceKey,
|
||||
oldData: view.mod.ModifiedBlock(resourceKey.Lang, resourceKey.ID, resourceKey.Index),
|
||||
newData: newData,
|
||||
}
|
||||
view.commander.Queue(command)
|
||||
}
|
||||
|
||||
func (view *View) currentBitmapKey() resource.Key {
|
||||
return view.currentBitmapKeyFor(view.model.currentBitmap)
|
||||
}
|
||||
|
||||
func (view *View) currentBitmapKeyFor(offset int) resource.Key {
|
||||
baseOffset := 1
|
||||
done := false
|
||||
view.mod.ObjectProperties().Iterate(func(triple object.Triple, prop *object.Properties) bool {
|
||||
if !done && triple == view.model.currentObject {
|
||||
done = true
|
||||
}
|
||||
if done {
|
||||
return false
|
||||
}
|
||||
numExtra := int(prop.Common.Bitmap3D.FrameNumber())
|
||||
baseOffset += 3 + numExtra
|
||||
return true
|
||||
})
|
||||
|
||||
return resource.KeyOf(ids.ObjectBitmaps, resource.LangAny, baseOffset+offset)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package objects
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
type viewModel struct {
|
||||
windowOpen bool
|
||||
restoreFocus bool
|
||||
|
||||
currentObject object.Triple
|
||||
currentBitmap int
|
||||
currentLang resource.Language
|
||||
}
|
||||
|
||||
func freshViewModel() viewModel {
|
||||
return viewModel{}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
|
||||
mgl "github.com/go-gl/mathgl/mgl32"
|
||||
)
|
||||
|
||||
// Context provides current render properties.
|
||||
type Context struct {
|
||||
OpenGL opengl.OpenGL
|
||||
|
||||
ViewMatrix *mgl.Mat4
|
||||
ProjectionMatrix mgl.Mat4
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
)
|
||||
|
||||
// FrameImage renders an image centered and fitted within the given size.
|
||||
func FrameImage(label string, frameCache *graphics.FrameCache, key graphics.FrameCacheKey, size imgui.Vec2) {
|
||||
textureID := TextureIDForBitmapFrame(key)
|
||||
|
||||
imgui.PushStyleColor(imgui.StyleColorChildBg, imgui.Vec4{X: 0, Y: 0, Z: 0, W: 1})
|
||||
imgui.PushStyleVarVec2(imgui.StyleVarWindowPadding, imgui.Vec2{X: 0, Y: 0})
|
||||
if imgui.BeginChildV(label, size, false,
|
||||
imgui.WindowFlagsNoNav|imgui.WindowFlagsNoInputs|imgui.WindowFlagsNoScrollWithMouse|
|
||||
imgui.WindowFlagsNoScrollbar) {
|
||||
texture := frameCache.Texture(key)
|
||||
if texture != nil {
|
||||
var uv imgui.Vec2
|
||||
uv.X, uv.Y = texture.UV()
|
||||
width, height := texture.Size()
|
||||
|
||||
scaleFactor := float32(math.Min(float64(size.X/width), float64(size.Y/height)))
|
||||
imageSize := imgui.Vec2{X: width * scaleFactor, Y: height * scaleFactor}
|
||||
|
||||
bufferSize := imgui.Vec2{X: (size.X - imageSize.X) / 2, Y: (size.Y - imageSize.Y) / 2}
|
||||
imgui.SetCursorPos(bufferSize)
|
||||
|
||||
imgui.ImageV(textureID, imageSize, imgui.Vec2{}, uv,
|
||||
imgui.Vec4{X: 1, Y: 1, Z: 1, W: 1}, imgui.Vec4{X: 0, Y: 0, Z: 0, W: 0})
|
||||
}
|
||||
}
|
||||
imgui.EndChild()
|
||||
imgui.PopStyleVar()
|
||||
imgui.PopStyleColor()
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
mgl "github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
)
|
||||
|
||||
var orientationViewVertexShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
in vec3 vertexPosition;
|
||||
|
||||
uniform mat4 modelMatrix;
|
||||
uniform mat4 viewMatrix;
|
||||
uniform mat4 projectionMatrix;
|
||||
|
||||
out vec3 position;
|
||||
out float zCenter;
|
||||
|
||||
void main(void) {
|
||||
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vertexPosition, 1.0);
|
||||
zCenter = (projectionMatrix * viewMatrix * modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).z;
|
||||
position = gl_Position.xyz;
|
||||
}
|
||||
`
|
||||
|
||||
var orientationViewFragmentShaderSource = `
|
||||
#version 150
|
||||
precision mediump float;
|
||||
|
||||
uniform vec4 foregroundColor;
|
||||
uniform vec4 backgroundColor;
|
||||
|
||||
in vec3 position;
|
||||
in float zCenter;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
void main(void) {
|
||||
if (position.z <= zCenter)
|
||||
{
|
||||
fragColor = foregroundColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
fragColor = backgroundColor;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// OrientationView is a control that displays how an object is oriented.
|
||||
// This is still not working properly. The orientation arrow is not properly rotated,
|
||||
// and I suspect the typical issue of three-angle rotation versus quaternion rotation.
|
||||
// Though I doubt the original engine used quaternions, it remains too confusing for me.
|
||||
type OrientationView struct {
|
||||
context Context
|
||||
|
||||
program uint32
|
||||
vao *opengl.VertexArrayObject
|
||||
vertexPositionBuffer uint32
|
||||
vertexPositionAttrib int32
|
||||
modelMatrixUniform opengl.Matrix4Uniform
|
||||
viewMatrixUniform opengl.Matrix4Uniform
|
||||
projectionMatrixUniform opengl.Matrix4Uniform
|
||||
foregroundColorUniform opengl.Vector4Uniform
|
||||
backgroundColorUniform opengl.Vector4Uniform
|
||||
|
||||
baseOrientation mgl.Mat4
|
||||
rotation mgl.Vec3
|
||||
xRingVerticesStart int
|
||||
yRingVerticesStart int
|
||||
zRingVerticesStart int
|
||||
vertices int
|
||||
}
|
||||
|
||||
// NewOrientationView returns a new instance.
|
||||
func NewOrientationView(context Context, baseOrientation mgl.Mat4, rotation mgl.Vec3) *OrientationView {
|
||||
gl := context.OpenGL
|
||||
program, programErr := opengl.LinkNewStandardProgram(gl, orientationViewVertexShaderSource, orientationViewFragmentShaderSource)
|
||||
|
||||
if programErr != nil {
|
||||
panic(opengl.NamedShaderError{Name: "OrientationViewShader", Nested: programErr})
|
||||
}
|
||||
|
||||
view := &OrientationView{
|
||||
context: context,
|
||||
program: program,
|
||||
|
||||
vao: opengl.NewVertexArrayObject(gl, program),
|
||||
vertexPositionBuffer: gl.GenBuffers(1)[0],
|
||||
vertexPositionAttrib: gl.GetAttribLocation(program, "vertexPosition"),
|
||||
modelMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "modelMatrix")),
|
||||
viewMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "viewMatrix")),
|
||||
projectionMatrixUniform: opengl.Matrix4Uniform(gl.GetUniformLocation(program, "projectionMatrix")),
|
||||
foregroundColorUniform: opengl.Vector4Uniform(gl.GetUniformLocation(program, "foregroundColor")),
|
||||
backgroundColorUniform: opengl.Vector4Uniform(gl.GetUniformLocation(program, "backgroundColor")),
|
||||
|
||||
baseOrientation: baseOrientation,
|
||||
rotation: rotation,
|
||||
}
|
||||
{
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, view.vertexPositionBuffer)
|
||||
var vertices []float32
|
||||
radius := 0.5
|
||||
vertices = append(vertices, 0.0, 0.0, 0.0)
|
||||
vertices = append(vertices, 0.4, 0.0, 0.0)
|
||||
vertices = append(vertices, 0.0, 0.0, 0.0)
|
||||
vertices = append(vertices, 0.0, 0.4, 0.0)
|
||||
vertices = append(vertices, 0.0, 0.0, 0.0)
|
||||
vertices = append(vertices, 0.0, 0.0, 0.4)
|
||||
|
||||
view.xRingVerticesStart = len(vertices)
|
||||
for angle := 0.0; angle < float64(toRad(360.0)); angle += float64(toRad(4.0)) {
|
||||
vertices = append(vertices, 0.0, float32(radius*math.Cos(angle)), float32(radius*math.Sin(angle)))
|
||||
}
|
||||
vertices = append(vertices, 0.0, float32(radius*math.Cos(0)), float32(radius*math.Sin(0)))
|
||||
|
||||
view.yRingVerticesStart = len(vertices)
|
||||
for angle := 0.0; angle < float64(toRad(360.0)); angle += float64(toRad(4.0)) {
|
||||
vertices = append(vertices, float32(radius*math.Cos(angle)), 0.0, float32(radius*math.Sin(angle)))
|
||||
}
|
||||
vertices = append(vertices, float32(radius*math.Cos(0)), 0.0, float32(radius*math.Sin(0)))
|
||||
|
||||
view.zRingVerticesStart = len(vertices)
|
||||
for angle := 0.0; angle < float64(toRad(360.0)); angle += float64(toRad(4.0)) {
|
||||
vertices = append(vertices, float32(radius*math.Cos(angle)), float32(radius*math.Sin(angle)), 0.0)
|
||||
}
|
||||
vertices = append(vertices, float32(radius*math.Cos(0)), float32(radius*math.Sin(0)), 0.0)
|
||||
|
||||
view.vertices = len(vertices)
|
||||
gl.BufferData(opengl.ARRAY_BUFFER, len(vertices)*4, vertices, opengl.STATIC_DRAW)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
}
|
||||
view.vao.WithSetter(func(gl opengl.OpenGL) {
|
||||
gl.EnableVertexAttribArray(uint32(view.vertexPositionAttrib))
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, view.vertexPositionBuffer)
|
||||
gl.VertexAttribOffset(uint32(view.vertexPositionAttrib), 3, opengl.FLOAT, false, 0, 0)
|
||||
gl.BindBuffer(opengl.ARRAY_BUFFER, 0)
|
||||
})
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
// Dispose cleans up resources of the view.
|
||||
func (view *OrientationView) Dispose() {
|
||||
gl := view.context.OpenGL
|
||||
view.vao.Dispose()
|
||||
gl.DeleteBuffers([]uint32{view.vertexPositionBuffer})
|
||||
gl.DeleteProgram(view.program)
|
||||
}
|
||||
|
||||
func toRad(degree float32) float32 {
|
||||
return (degree * math.Pi * 2.0) / 360.0
|
||||
}
|
||||
|
||||
// Render renders the orientation view for given orientation.
|
||||
func (view *OrientationView) Render(orientation mgl.Vec3) {
|
||||
gl := view.context.OpenGL
|
||||
|
||||
view.vao.OnShader(func() {
|
||||
view.projectionMatrixUniform.Set(gl, &view.context.ProjectionMatrix)
|
||||
view.viewMatrixUniform.Set(gl, view.context.ViewMatrix)
|
||||
|
||||
view.renderRings(false)
|
||||
view.renderArrow(
|
||||
mgl.Ident4().
|
||||
Mul4(mgl.HomogRotate3D(toRad(orientation[0]), mgl.Vec3{view.rotation[0], 0.0, 0.0})).
|
||||
Mul4(mgl.HomogRotate3D(toRad(orientation[1]), mgl.Vec3{0.0, view.rotation[1], 0.0})).
|
||||
Mul4(mgl.HomogRotate3D(toRad(orientation[2]), mgl.Vec3{0.0, 0.0, view.rotation[2]})))
|
||||
view.renderRings(true)
|
||||
})
|
||||
}
|
||||
|
||||
func (view *OrientationView) renderRings(front bool) {
|
||||
foreground := func(color [4]float32) [4]float32 {
|
||||
if !front {
|
||||
return [4]float32{0.0, 0.0, 0.0, 0.0}
|
||||
}
|
||||
return color
|
||||
}
|
||||
background := func(color [4]float32) [4]float32 {
|
||||
if front {
|
||||
return [4]float32{0.0, 0.0, 0.0, 0.0}
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
// draw Z-rotation ring
|
||||
view.renderRing(view.zRingVerticesStart, view.vertices,
|
||||
foreground([4]float32{0.0, 0.0, 1.0, 1.0}), background([4]float32{0.0, 0.0, 0.8, 0.7}))
|
||||
|
||||
// draw Y-rotation ring
|
||||
view.renderRing(view.yRingVerticesStart, view.zRingVerticesStart,
|
||||
foreground([4]float32{0.0, 1.0, 0.0, 1.0}), background([4]float32{0.0, 0.8, 0.0, 0.7}))
|
||||
|
||||
// draw X-rotation ring
|
||||
view.renderRing(view.xRingVerticesStart, view.yRingVerticesStart,
|
||||
foreground([4]float32{1.0, 0.0, 0.0, 1.0}), background([4]float32{0.8, 0.0, 0.0, 0.7}))
|
||||
}
|
||||
|
||||
func (view *OrientationView) renderRing(start, end int, foregroundColor, backgroundColor [4]float32) {
|
||||
gl := view.context.OpenGL
|
||||
|
||||
modelMatrix := mgl.Ident4().Mul4(view.baseOrientation)
|
||||
view.foregroundColorUniform.Set(gl, &foregroundColor)
|
||||
view.backgroundColorUniform.Set(gl, &backgroundColor)
|
||||
view.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||
gl.DrawArrays(opengl.LINES, int32(start)/3, int32(end-start)/3)
|
||||
}
|
||||
|
||||
func (view *OrientationView) renderArrow(rotation mgl.Mat4) {
|
||||
gl := view.context.OpenGL
|
||||
|
||||
modelMatrix := mgl.Ident4().Mul4(view.baseOrientation).Mul4(rotation)
|
||||
view.foregroundColorUniform.Set(gl, &[4]float32{0.0, 0.0, 1.0, 1.0})
|
||||
view.backgroundColorUniform.Set(gl, &[4]float32{0.0, 0.0, 0.6, 1.0})
|
||||
view.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||
gl.DrawArrays(opengl.LINES, 4, 2)
|
||||
|
||||
modelMatrix = mgl.Ident4().Mul4(view.baseOrientation).Mul4(rotation)
|
||||
view.foregroundColorUniform.Set(gl, &[4]float32{0.0, 1.0, 0.0, 1.0})
|
||||
view.backgroundColorUniform.Set(gl, &[4]float32{0.0, 0.6, 0.0, 1.0})
|
||||
view.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||
gl.DrawArrays(opengl.LINES, 2, 2)
|
||||
|
||||
modelMatrix = mgl.Ident4().Mul4(view.baseOrientation).Mul4(rotation)
|
||||
view.foregroundColorUniform.Set(gl, &[4]float32{1.0, 0.0, 0.0, 1.0})
|
||||
view.backgroundColorUniform.Set(gl, &[4]float32{0.6, 0.0, 0.0, 1.0})
|
||||
view.modelMatrixUniform.Set(gl, &modelMatrix)
|
||||
gl.DrawArrays(opengl.LINES, 0, 2)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
const (
|
||||
// BitmapTextureTypeResource specifies a bitmap texture directly from resources.
|
||||
BitmapTextureTypeResource = 0x00
|
||||
// BitmapTextureTypeFrame specifies a bitmap texture from the frame cache.
|
||||
BitmapTextureTypeFrame = 0x01
|
||||
)
|
||||
|
||||
// TextureIDForBitmapTexture returns a texture ID that identifies a bitmap texture from resources.
|
||||
func TextureIDForBitmapTexture(key resource.Key) imgui.TextureID {
|
||||
id := imgui.TextureID(gui.ImageTypeBitmapTexture) << 56
|
||||
id |= imgui.TextureID(BitmapTextureTypeResource) << 48
|
||||
id |= imgui.TextureID(key.Lang) << 32
|
||||
id |= imgui.TextureID(key.ID) << 16
|
||||
id |= imgui.TextureID(key.Index & 0xFFFF)
|
||||
return id
|
||||
}
|
||||
|
||||
// TextureIDForBitmapFrame returns a texture ID that identifies a bitmap texture from frame cache.
|
||||
func TextureIDForBitmapFrame(key graphics.FrameCacheKey) imgui.TextureID {
|
||||
id := imgui.TextureID(gui.ImageTypeBitmapTexture) << 56
|
||||
id |= imgui.TextureID(BitmapTextureTypeFrame) << 48
|
||||
id |= imgui.TextureID(key)
|
||||
return id
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
// TextureImage renders an image centered and fitted within the given size.
|
||||
func TextureImage(label string, cache *graphics.TextureCache, key resource.Key, size imgui.Vec2) {
|
||||
textureID := TextureIDForBitmapTexture(key)
|
||||
|
||||
imgui.PushStyleColor(imgui.StyleColorChildBg, imgui.Vec4{X: 0, Y: 0, Z: 0, W: 1})
|
||||
imgui.PushStyleVarVec2(imgui.StyleVarWindowPadding, imgui.Vec2{X: 0, Y: 0})
|
||||
if imgui.BeginChildV(label, size, false,
|
||||
imgui.WindowFlagsNoNav|imgui.WindowFlagsNoInputs|imgui.WindowFlagsNoScrollWithMouse|
|
||||
imgui.WindowFlagsNoScrollbar) {
|
||||
texture, err := cache.Texture(key)
|
||||
if err == nil {
|
||||
var uv imgui.Vec2
|
||||
uv.X, uv.Y = texture.UV()
|
||||
width, height := texture.Size()
|
||||
|
||||
scaleFactor := float32(math.Min(float64(size.X/width), float64(size.Y/height)))
|
||||
imageSize := imgui.Vec2{X: width * scaleFactor, Y: height * scaleFactor}
|
||||
|
||||
bufferSize := imgui.Vec2{X: (size.X - imageSize.X) / 2, Y: (size.Y - imageSize.Y) / 2}
|
||||
imgui.SetCursorPos(bufferSize)
|
||||
|
||||
imgui.ImageV(textureID, imageSize, imgui.Vec2{}, uv,
|
||||
imgui.Vec4{X: 1, Y: 1, Z: 1, W: 1}, imgui.Vec4{X: 0, Y: 0, Z: 0, W: 0})
|
||||
}
|
||||
}
|
||||
imgui.EndChild()
|
||||
imgui.PopStyleVar()
|
||||
imgui.PopStyleColor()
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/inkyblackness/hacked/ui/opengl"
|
||||
)
|
||||
|
||||
// TextureRenderer is a renderer within which something can be rendered onto a texture.
|
||||
// This texture can then be used to be displayed.
|
||||
type TextureRenderer struct {
|
||||
gl opengl.OpenGL
|
||||
|
||||
framebuffer uint32
|
||||
texture uint32
|
||||
|
||||
width int32
|
||||
height int32
|
||||
}
|
||||
|
||||
// NewTextureRenderer returns a new instance.
|
||||
func NewTextureRenderer(gl opengl.OpenGL) *TextureRenderer {
|
||||
renderer := &TextureRenderer{
|
||||
gl: gl,
|
||||
|
||||
framebuffer: gl.GenFramebuffers(1)[0],
|
||||
texture: gl.GenTextures(1)[0],
|
||||
|
||||
width: 200,
|
||||
height: 200,
|
||||
}
|
||||
|
||||
gl.BindTexture(opengl.TEXTURE_2D, renderer.texture)
|
||||
gl.TexParameteri(opengl.TEXTURE_2D, opengl.TEXTURE_MAG_FILTER, opengl.NEAREST)
|
||||
gl.TexParameteri(opengl.TEXTURE_2D, opengl.TEXTURE_MIN_FILTER, opengl.NEAREST)
|
||||
offset := unsafe.Pointer(uintptr(0)) // nolint: govet
|
||||
gl.TexImage2D(opengl.TEXTURE_2D, 0, opengl.RGBA, renderer.width, renderer.height, 0, opengl.RGBA, opengl.UNSIGNED_BYTE, offset)
|
||||
// gl.GenerateMipmap(opengl.TEXTURE_2D)
|
||||
gl.BindTexture(opengl.TEXTURE_2D, 0)
|
||||
|
||||
renderer.onFramebuffer(func() {
|
||||
gl.BindTexture(opengl.TEXTURE_2D, renderer.texture)
|
||||
gl.FramebufferTexture(opengl.FRAMEBUFFER, opengl.COLOR_ATTACHMENT0, renderer.texture, 0)
|
||||
gl.DrawBuffers([]uint32{opengl.COLOR_ATTACHMENT0})
|
||||
gl.BindTexture(opengl.TEXTURE_2D, 0)
|
||||
|
||||
// result := gl.CheckFramebufferStatus(opengl.FRAMEBUFFER)
|
||||
// fmt.Printf("status: 0x%04X\n", result)
|
||||
// result == FRAMEBUFFER_COMPLETE == 0x8CD5
|
||||
})
|
||||
return renderer
|
||||
}
|
||||
|
||||
// Dispose cleans up resources.
|
||||
func (renderer *TextureRenderer) Dispose() {
|
||||
gl := renderer.gl
|
||||
gl.DeleteFramebuffers([]uint32{renderer.framebuffer})
|
||||
gl.DeleteTextures([]uint32{renderer.texture})
|
||||
}
|
||||
|
||||
// Handle returns the texture handle.
|
||||
func (renderer *TextureRenderer) Handle() uint32 {
|
||||
return renderer.texture
|
||||
}
|
||||
|
||||
// Size returns the dimensions of the texture, in pixels.
|
||||
func (renderer *TextureRenderer) Size() (width, height float32) {
|
||||
return float32(renderer.width), float32(renderer.height)
|
||||
}
|
||||
|
||||
// Render calls the nested function. All drawing commands within this function will end up in the texture.
|
||||
func (renderer *TextureRenderer) Render(nested func()) {
|
||||
gl := renderer.gl
|
||||
renderer.onFramebuffer(func() {
|
||||
gl.Viewport(0, 0, renderer.width, renderer.height)
|
||||
gl.ClearColor(0.0, 0.0, 0.0, 0.0)
|
||||
gl.Clear(opengl.COLOR_BUFFER_BIT | opengl.DEPTH_BUFFER_BIT)
|
||||
|
||||
nested()
|
||||
})
|
||||
}
|
||||
|
||||
func (renderer *TextureRenderer) onFramebuffer(nested func()) {
|
||||
gl := renderer.gl
|
||||
gl.BindFramebuffer(opengl.FRAMEBUFFER, renderer.framebuffer)
|
||||
nested()
|
||||
gl.BindFramebuffer(opengl.FRAMEBUFFER, 0)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
// TextureSelector renders a horizontal list of game textures, with selection capability.
|
||||
func TextureSelector(label string, width float32, guiScale float32,
|
||||
count int, selectedIndex int, cache *graphics.TextureCache, keyResolver func(int) resource.Key,
|
||||
tooltipText func(int) string,
|
||||
changeCallback func(int)) {
|
||||
if imgui.BeginChildV(label, imgui.Vec2{X: width * guiScale, Y: 100 * guiScale}, true,
|
||||
imgui.WindowFlagsHorizontalScrollbar|imgui.WindowFlagsNoScrollWithMouse) {
|
||||
for i := 0; i < count; i++ {
|
||||
key := keyResolver(i)
|
||||
imgui.PushStyleVarVec2(imgui.StyleVarWindowPadding, imgui.Vec2{X: 0, Y: 0})
|
||||
if imgui.BeginChildV(fmt.Sprintf("%3d", i), imgui.Vec2{X: 80 * guiScale, Y: 64 * guiScale}, false,
|
||||
imgui.WindowFlagsNoNav|imgui.WindowFlagsNoScrollWithMouse) {
|
||||
imgui.BeginGroup()
|
||||
if imgui.SelectableV("", selectedIndex == i, 0, imgui.Vec2{X: 0, Y: 64 * guiScale}) {
|
||||
changeCallback(i)
|
||||
}
|
||||
imgui.SameLine()
|
||||
TextureImage(fmt.Sprintf("%3d", i), cache, key, imgui.Vec2{X: 64 * guiScale, Y: 64 * guiScale})
|
||||
imgui.EndGroup()
|
||||
if imgui.IsItemHovered() {
|
||||
text := tooltipText(i)
|
||||
if len(text) > 0 {
|
||||
imgui.SetTooltip(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
imgui.EndChild()
|
||||
imgui.PopStyleVar()
|
||||
imgui.SameLine()
|
||||
}
|
||||
}
|
||||
imgui.EndChild()
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package sounds
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/external"
|
||||
"github.com/inkyblackness/hacked/ss1/content/audio"
|
||||
"github.com/inkyblackness/hacked/ss1/edit/undoable"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
// View provides edit controls for sound effects.
|
||||
type View struct {
|
||||
soundEffectService undoable.SoundEffectService
|
||||
|
||||
modalStateMachine gui.ModalStateMachine
|
||||
guiScale float32
|
||||
|
||||
model viewModel
|
||||
}
|
||||
|
||||
// NewSoundEffectsView returns a new instance.
|
||||
func NewSoundEffectsView(soundEffectService undoable.SoundEffectService,
|
||||
modalStateMachine gui.ModalStateMachine,
|
||||
guiScale float32) *View {
|
||||
view := &View{
|
||||
soundEffectService: soundEffectService,
|
||||
|
||||
modalStateMachine: modalStateMachine,
|
||||
guiScale: guiScale,
|
||||
|
||||
model: freshViewModel(),
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// WindowOpen returns the flag address, to be used with the main menu.
|
||||
func (view *View) WindowOpen() *bool {
|
||||
return &view.model.windowOpen
|
||||
}
|
||||
|
||||
// Render renders the view.
|
||||
func (view *View) Render() {
|
||||
if view.model.restoreFocus {
|
||||
imgui.SetNextWindowFocus()
|
||||
view.model.restoreFocus = false
|
||||
view.model.windowOpen = true
|
||||
}
|
||||
if view.model.windowOpen {
|
||||
imgui.SetNextWindowSizeV(imgui.Vec2{X: 400 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||
if imgui.BeginV("Sound Effects", view.WindowOpen(), imgui.WindowFlagsNoCollapse) {
|
||||
view.renderContent()
|
||||
}
|
||||
imgui.End()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) renderContent() {
|
||||
info, _ := ids.Info(view.model.currentKey.ID)
|
||||
|
||||
imgui.BeginChildV("SoundEffects", imgui.Vec2{X: -1, Y: -60 * view.guiScale}, true, 0)
|
||||
for i := 0; i < info.MaxCount; i++ {
|
||||
effects := ids.SoundEffectsForAudio(i)
|
||||
text := fmt.Sprintf("%3d", i)
|
||||
if len(effects) > 0 {
|
||||
text += " - "
|
||||
for effectIndex, effect := range effects {
|
||||
if effectIndex > 0 {
|
||||
text += ", "
|
||||
}
|
||||
text += effect.Name
|
||||
}
|
||||
} else {
|
||||
text += " (unidentified)"
|
||||
}
|
||||
if imgui.SelectableV(text, view.model.currentKey.Index == i, 0, imgui.Vec2{}) {
|
||||
view.model.currentKey.Index = i
|
||||
}
|
||||
}
|
||||
imgui.EndChild()
|
||||
|
||||
imgui.PushItemWidth(-100 * view.guiScale)
|
||||
sound := view.currentSound()
|
||||
if !sound.Empty() {
|
||||
imgui.LabelText("Audio", fmt.Sprintf("%.2f sec", sound.Duration()))
|
||||
if imgui.Button("Export") {
|
||||
view.requestExportAudio(sound)
|
||||
}
|
||||
imgui.SameLine()
|
||||
} else {
|
||||
imgui.LabelText("Audio", "(no sound)")
|
||||
}
|
||||
if imgui.Button("Import") {
|
||||
view.requestImportAudio()
|
||||
}
|
||||
imgui.PopItemWidth()
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Clear") {
|
||||
view.clearAudio()
|
||||
}
|
||||
if view.soundEffectService.Modified(view.model.currentKey) {
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Remove") {
|
||||
view.removeAudio()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) clearAudio() {
|
||||
view.soundEffectService.RequestClear(view.model.currentKey, view.restoreFunc())
|
||||
}
|
||||
|
||||
func (view *View) removeAudio() {
|
||||
view.soundEffectService.RequestRemove(view.model.currentKey, view.restoreFunc())
|
||||
}
|
||||
|
||||
func (view *View) currentSound() (sound audio.L8) {
|
||||
return view.soundEffectService.Audio(view.model.currentKey)
|
||||
}
|
||||
|
||||
func (view *View) requestExportAudio(sound audio.L8) {
|
||||
filename := fmt.Sprintf("sfx_%03d.wav", view.model.currentKey.Index)
|
||||
|
||||
external.ExportAudio(view.modalStateMachine, filename, sound)
|
||||
}
|
||||
|
||||
func (view *View) requestImportAudio() {
|
||||
external.ImportAudio(view.modalStateMachine, func(sound audio.L8) {
|
||||
view.soundEffectService.RequestSetAudio(view.model.currentKey, sound, view.restoreFunc())
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) restoreFunc() func() {
|
||||
oldKey := view.model.currentKey
|
||||
|
||||
return func() {
|
||||
view.model.restoreFocus = true
|
||||
view.model.currentKey = oldKey
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package sounds
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
type viewModel struct {
|
||||
windowOpen bool
|
||||
restoreFocus bool
|
||||
currentKey resource.Key
|
||||
}
|
||||
|
||||
func freshViewModel() viewModel {
|
||||
return viewModel{
|
||||
currentKey: resource.KeyOf(ids.SoundEffectsAudioStart, resource.LangDefault, 0),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package texts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/external"
|
||||
"github.com/inkyblackness/hacked/ss1/content/audio"
|
||||
"github.com/inkyblackness/hacked/ss1/edit"
|
||||
"github.com/inkyblackness/hacked/ss1/edit/undoable"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
// View provides edit controls for texts.
|
||||
type View struct {
|
||||
textService undoable.AugmentedTextService
|
||||
|
||||
modalStateMachine gui.ModalStateMachine
|
||||
clipboard external.Clipboard
|
||||
guiScale float32
|
||||
|
||||
model viewModel
|
||||
}
|
||||
|
||||
// NewTextsView returns a new instance.
|
||||
func NewTextsView(textService undoable.AugmentedTextService,
|
||||
modalStateMachine gui.ModalStateMachine, clipboard external.Clipboard,
|
||||
guiScale float32) *View {
|
||||
view := &View{
|
||||
textService: textService,
|
||||
|
||||
modalStateMachine: modalStateMachine,
|
||||
clipboard: clipboard,
|
||||
guiScale: guiScale,
|
||||
|
||||
model: freshViewModel(),
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// WindowOpen returns the flag address, to be used with the main menu.
|
||||
func (view *View) WindowOpen() *bool {
|
||||
return &view.model.windowOpen
|
||||
}
|
||||
|
||||
// Render renders the view.
|
||||
func (view *View) Render() {
|
||||
if view.model.restoreFocus {
|
||||
imgui.SetNextWindowFocus()
|
||||
view.model.restoreFocus = false
|
||||
view.model.windowOpen = true
|
||||
}
|
||||
if view.model.windowOpen {
|
||||
imgui.SetNextWindowSizeV(imgui.Vec2{X: 400 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||
if imgui.BeginV("Texts", view.WindowOpen(), imgui.WindowFlagsNoCollapse) {
|
||||
view.renderContent()
|
||||
}
|
||||
imgui.End()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) renderContent() {
|
||||
knownTexts := edit.KnownTexts()
|
||||
imgui.PushItemWidth(-100 * view.guiScale)
|
||||
if imgui.BeginCombo("Text Type", knownTexts.Title(view.model.currentKey.ID)) {
|
||||
for _, info := range knownTexts {
|
||||
if imgui.SelectableV(info.Title, info.ID == view.model.currentKey.ID, 0, imgui.Vec2{}) {
|
||||
view.model.currentKey.ID = info.ID
|
||||
view.model.currentKey.Index = 0
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
info, _ := ids.Info(view.model.currentKey.ID)
|
||||
gui.StepSliderInt("Index", &view.model.currentKey.Index, 0, info.MaxCount-1)
|
||||
|
||||
if imgui.BeginCombo("Language", view.model.currentKey.Lang.String()) {
|
||||
languages := resource.Languages()
|
||||
for _, lang := range languages {
|
||||
if imgui.SelectableV(lang.String(), lang == view.model.currentKey.Lang, 0, imgui.Vec2{}) {
|
||||
view.model.currentKey.Lang = lang
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
|
||||
if view.textIsWithAudio() {
|
||||
imgui.Separator()
|
||||
sound := view.currentSound()
|
||||
if !sound.Empty() {
|
||||
imgui.LabelText("Audio", fmt.Sprintf("%.2f sec", sound.Duration()))
|
||||
if imgui.Button("Export") {
|
||||
view.requestExportAudio(sound)
|
||||
}
|
||||
imgui.SameLine()
|
||||
} else {
|
||||
imgui.LabelText("Audio", "(no sound)")
|
||||
}
|
||||
if imgui.Button("Import") {
|
||||
view.requestImportAudio()
|
||||
}
|
||||
}
|
||||
imgui.Separator()
|
||||
|
||||
imgui.PopItemWidth()
|
||||
|
||||
currentText := view.currentText()
|
||||
imgui.BeginChildV("Text", imgui.Vec2{X: -100 * view.guiScale, Y: 0}, true, 0)
|
||||
imgui.PushTextWrapPos()
|
||||
if len(currentText) == 0 {
|
||||
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1.0, Y: 1.0, Z: 1.0, W: 0.5})
|
||||
imgui.Text("(empty)")
|
||||
imgui.PopStyleColor()
|
||||
} else {
|
||||
imgui.Text(currentText)
|
||||
}
|
||||
imgui.PopTextWrapPos()
|
||||
imgui.EndChild()
|
||||
|
||||
imgui.SameLine()
|
||||
|
||||
imgui.BeginGroup()
|
||||
if imgui.ButtonV("-> Clip", imgui.Vec2{X: -1, Y: 0}) {
|
||||
view.copyTextToClipboard(currentText)
|
||||
}
|
||||
if imgui.ButtonV("<- Clip", imgui.Vec2{X: -1, Y: 0}) {
|
||||
view.setTextFromClipboard()
|
||||
}
|
||||
if imgui.ButtonV("Clear", imgui.Vec2{X: -1, Y: 0}) {
|
||||
view.clearText()
|
||||
}
|
||||
if imgui.ButtonV("Remove", imgui.Vec2{X: -1, Y: 0}) {
|
||||
view.removeText()
|
||||
}
|
||||
imgui.EndGroup()
|
||||
}
|
||||
|
||||
func (view View) currentText() string {
|
||||
return view.textService.Text(view.model.currentKey)
|
||||
}
|
||||
|
||||
func (view View) copyTextToClipboard(text string) {
|
||||
if len(text) > 0 {
|
||||
view.clipboard.SetString(text)
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) setTextFromClipboard() {
|
||||
value, err := view.clipboard.String()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
view.textService.RequestSetText(view.model.currentKey, value, view.restoreFunc())
|
||||
}
|
||||
|
||||
func (view *View) clearText() {
|
||||
view.textService.RequestClear(view.model.currentKey, view.restoreFunc())
|
||||
}
|
||||
|
||||
func (view *View) removeText() {
|
||||
view.textService.RequestRemove(view.model.currentKey, view.restoreFunc())
|
||||
}
|
||||
|
||||
func (view View) textIsWithAudio() bool {
|
||||
return view.textService.WithAudio(view.model.currentKey)
|
||||
}
|
||||
|
||||
func (view *View) currentSound() (sound audio.L8) {
|
||||
return view.textService.Sound(view.model.currentKey)
|
||||
}
|
||||
|
||||
func (view *View) requestExportAudio(sound audio.L8) {
|
||||
if !view.textIsWithAudio() {
|
||||
return
|
||||
}
|
||||
filename := fmt.Sprintf("%05d_%03d_%s.wav", view.model.currentKey.ID, view.model.currentKey.Index, view.model.currentKey.Lang.String())
|
||||
|
||||
external.ExportAudio(view.modalStateMachine, filename, sound)
|
||||
}
|
||||
|
||||
func (view *View) requestImportAudio() {
|
||||
external.ImportAudio(view.modalStateMachine, func(sound audio.L8) {
|
||||
view.textService.RequestSetSound(view.model.currentKey, sound, view.restoreFunc())
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) restoreFunc() func() {
|
||||
oldKey := view.model.currentKey
|
||||
|
||||
return func() {
|
||||
view.model.restoreFocus = true
|
||||
view.model.currentKey = oldKey
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package texts
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/edit"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
type viewModel struct {
|
||||
windowOpen bool
|
||||
restoreFocus bool
|
||||
currentKey resource.Key
|
||||
}
|
||||
|
||||
func freshViewModel() viewModel {
|
||||
return viewModel{
|
||||
currentKey: resource.KeyOf(edit.KnownTexts()[0].ID, resource.LangDefault, 0),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package textures
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
type setTextureBitmapCommand struct {
|
||||
model *viewModel
|
||||
|
||||
textureIndex int
|
||||
id resource.ID
|
||||
|
||||
oldData []byte
|
||||
newData []byte
|
||||
}
|
||||
|
||||
func (command setTextureBitmapCommand) Do(modder world.Modder) error {
|
||||
return command.perform(modder, command.newData)
|
||||
}
|
||||
|
||||
func (command setTextureBitmapCommand) Undo(modder world.Modder) error {
|
||||
return command.perform(modder, command.oldData)
|
||||
}
|
||||
|
||||
func (command setTextureBitmapCommand) perform(modder world.Modder, data []byte) error {
|
||||
info, existing := ids.Info(command.id)
|
||||
if !existing {
|
||||
panic(fmt.Sprintf("unknown identifier for bitmap resource: %v", command.id))
|
||||
}
|
||||
resourceID := command.id
|
||||
blockIndex := command.textureIndex
|
||||
if !info.List {
|
||||
resourceID = resourceID.Plus(blockIndex)
|
||||
blockIndex = 0
|
||||
}
|
||||
|
||||
if (len(data) > 0) || info.List {
|
||||
modder.SetResourceBlock(resource.LangAny, resourceID, blockIndex, data)
|
||||
} else {
|
||||
modder.DelResource(resource.LangAny, resourceID)
|
||||
}
|
||||
|
||||
command.model.restoreFocus = true
|
||||
command.model.currentIndex = command.textureIndex
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package textures
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/content/texture"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
)
|
||||
|
||||
type setTexturePropertiesCommand struct {
|
||||
model *viewModel
|
||||
|
||||
textureIndex int
|
||||
|
||||
oldProperties texture.Properties
|
||||
newProperties texture.Properties
|
||||
}
|
||||
|
||||
func (cmd setTexturePropertiesCommand) Do(modder world.Modder) error {
|
||||
return cmd.perform(modder, cmd.newProperties)
|
||||
}
|
||||
|
||||
func (cmd setTexturePropertiesCommand) Undo(modder world.Modder) error {
|
||||
return cmd.perform(modder, cmd.oldProperties)
|
||||
}
|
||||
|
||||
func (cmd setTexturePropertiesCommand) perform(modder world.Modder, properties texture.Properties) error {
|
||||
modder.SetTextureProperties(cmd.textureIndex, properties)
|
||||
|
||||
cmd.model.restoreFocus = true
|
||||
cmd.model.currentIndex = cmd.textureIndex
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package textures
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world"
|
||||
)
|
||||
|
||||
type setTextureTextCommand struct {
|
||||
model *viewModel
|
||||
|
||||
key resource.Key
|
||||
|
||||
oldData []byte
|
||||
newData []byte
|
||||
}
|
||||
|
||||
func (command setTextureTextCommand) Do(modder world.Modder) error {
|
||||
return command.perform(modder, command.newData)
|
||||
}
|
||||
|
||||
func (command setTextureTextCommand) Undo(modder world.Modder) error {
|
||||
return command.perform(modder, command.oldData)
|
||||
}
|
||||
|
||||
func (command setTextureTextCommand) perform(modder world.Modder, data []byte) error {
|
||||
modder.SetResourceBlock(command.key.Lang, command.key.ID, command.key.Index, data)
|
||||
command.model.restoreFocus = true
|
||||
command.model.currentIndex = command.key.Index
|
||||
command.model.currentLang = command.key.Lang
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
package textures
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/external"
|
||||
"github.com/inkyblackness/hacked/editor/graphics"
|
||||
"github.com/inkyblackness/hacked/editor/render"
|
||||
"github.com/inkyblackness/hacked/editor/values"
|
||||
"github.com/inkyblackness/hacked/ss1/content/bitmap"
|
||||
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||
"github.com/inkyblackness/hacked/ss1/content/texture"
|
||||
"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"
|
||||
)
|
||||
|
||||
// View provides edit controls for textures.
|
||||
type View struct {
|
||||
mod *world.Mod
|
||||
textCache *text.Cache
|
||||
cp text.Codepage
|
||||
imageCache *graphics.TextureCache
|
||||
paletteCache *graphics.PaletteCache
|
||||
|
||||
modalStateMachine gui.ModalStateMachine
|
||||
clipboard external.Clipboard
|
||||
guiScale float32
|
||||
commander cmd.Commander
|
||||
|
||||
model viewModel
|
||||
}
|
||||
|
||||
// NewTexturesView returns a new instance.
|
||||
func NewTexturesView(mod *world.Mod, textCache *text.Cache, cp text.Codepage,
|
||||
imageCache *graphics.TextureCache, paletteCache *graphics.PaletteCache,
|
||||
modalStateMachine gui.ModalStateMachine,
|
||||
clipboard external.Clipboard, guiScale float32, commander cmd.Commander) *View {
|
||||
view := &View{
|
||||
mod: mod,
|
||||
textCache: textCache,
|
||||
cp: cp,
|
||||
imageCache: imageCache,
|
||||
paletteCache: paletteCache,
|
||||
|
||||
modalStateMachine: modalStateMachine,
|
||||
clipboard: clipboard,
|
||||
guiScale: guiScale,
|
||||
commander: commander,
|
||||
|
||||
model: freshViewModel(),
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
// WindowOpen returns the flag address, to be used with the main menu.
|
||||
func (view *View) WindowOpen() *bool {
|
||||
return &view.model.windowOpen
|
||||
}
|
||||
|
||||
// Render renders the view.
|
||||
func (view *View) Render() {
|
||||
if view.model.restoreFocus {
|
||||
imgui.SetNextWindowFocus()
|
||||
view.model.restoreFocus = false
|
||||
view.model.windowOpen = true
|
||||
}
|
||||
if view.model.windowOpen {
|
||||
imgui.SetNextWindowSizeV(imgui.Vec2{X: 800 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
|
||||
if imgui.BeginV("Textures", view.WindowOpen(), imgui.WindowFlagsNoCollapse|imgui.WindowFlagsHorizontalScrollbar) {
|
||||
view.renderContent()
|
||||
}
|
||||
imgui.End()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) renderContent() {
|
||||
if imgui.BeginChildV("Properties", imgui.Vec2{X: -300 * view.guiScale, Y: 0}, false, imgui.WindowFlagsHorizontalScrollbar) {
|
||||
imgui.PushItemWidth(-200 * view.guiScale)
|
||||
|
||||
gui.StepSliderInt("Index", &view.model.currentIndex, 0, world.MaxWorldTextures-1)
|
||||
|
||||
render.TextureSelector("###IndexBitmap", -1, view.guiScale, world.MaxWorldTextures,
|
||||
view.model.currentIndex, view.imageCache,
|
||||
func(index int) resource.Key {
|
||||
return view.indexedResourceKey(ids.LargeTextures, index)
|
||||
},
|
||||
view.textureTooltip,
|
||||
func(newValue int) {
|
||||
view.model.currentIndex = newValue
|
||||
})
|
||||
|
||||
readOnly := !view.mod.HasModifiableTextureProperties()
|
||||
|
||||
imgui.Separator()
|
||||
if imgui.BeginCombo("Language", view.model.currentLang.String()) {
|
||||
languages := resource.Languages()
|
||||
for _, lang := range languages {
|
||||
if imgui.SelectableV(lang.String(), lang == view.model.currentLang, 0, imgui.Vec2{}) {
|
||||
view.model.currentLang = lang
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
|
||||
nameKey := resource.KeyOf(ids.TextureNames, view.model.currentLang, view.model.currentIndex)
|
||||
name, _ := view.textCache.Text(nameKey)
|
||||
view.renderText(readOnly, "Name", name, view.requestSetTextureName)
|
||||
|
||||
useKey := resource.KeyOf(ids.TextureUsages, view.model.currentLang, view.model.currentIndex)
|
||||
use, _ := view.textCache.Text(useKey)
|
||||
view.renderText(readOnly, "Use", use, view.requestSetTextureUsage)
|
||||
|
||||
imgui.Separator()
|
||||
view.renderTextureProperties(readOnly)
|
||||
|
||||
imgui.PopItemWidth()
|
||||
}
|
||||
imgui.EndChild()
|
||||
imgui.SameLine()
|
||||
|
||||
imgui.BeginGroup()
|
||||
view.renderTextureSample("Large", ids.LargeTextures, 128, "Large")
|
||||
view.renderTextureSample("Medium", ids.MediumTextures, 64, "Medium")
|
||||
view.renderTextureSample("Small", ids.SmallTextures, 32, "Small")
|
||||
view.renderTextureSample("Icon", ids.IconTextures, 16, "Icon")
|
||||
imgui.EndGroup()
|
||||
}
|
||||
|
||||
func (view *View) renderText(readOnly bool, label string, value string, changeCallback func(string)) {
|
||||
imgui.LabelText(label, value)
|
||||
view.clipboardPopup(readOnly, label, value, changeCallback)
|
||||
}
|
||||
|
||||
func (view *View) clipboardPopup(readOnly bool, label string, value string, changeCallback func(string)) {
|
||||
if imgui.BeginPopupContextItemV(label+"-Popup", 1) {
|
||||
if imgui.Selectable("Copy to Clipboard") {
|
||||
view.clipboard.SetString(value)
|
||||
}
|
||||
if !readOnly && imgui.Selectable("Copy from Clipboard") {
|
||||
newValue, err := view.clipboard.String()
|
||||
if err == nil {
|
||||
changeCallback(newValue)
|
||||
}
|
||||
}
|
||||
imgui.EndPopup()
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) renderTextureProperties(readOnly bool) {
|
||||
list := view.mod.TextureProperties()
|
||||
distanceUnifier := values.NewUnifier()
|
||||
climbableUnifier := values.NewUnifier()
|
||||
transparencyControlUnifier := values.NewUnifier()
|
||||
animationGroupUnifier := values.NewUnifier()
|
||||
animationIndexUnifier := values.NewUnifier()
|
||||
if view.model.currentIndex < len(list) {
|
||||
properties := list[view.model.currentIndex]
|
||||
distanceUnifier.Add(int(properties.DistanceModifier))
|
||||
climbableUnifier.Add(properties.Climbable != 0)
|
||||
transparencyControlUnifier.Add(int(properties.TransparencyControl))
|
||||
animationGroupUnifier.Add(int(properties.AnimationGroup))
|
||||
animationIndexUnifier.Add(int(properties.AnimationIndex))
|
||||
}
|
||||
|
||||
values.RenderUnifiedSliderInt(readOnly, "Distance Modifier", distanceUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string { return "%d" },
|
||||
math.MinInt16, math.MaxInt16,
|
||||
func(newValue int) {
|
||||
view.requestChangeProperties(func(prop *texture.Properties) {
|
||||
prop.DistanceModifier = int16(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
values.RenderUnifiedCheckboxCombo(readOnly, "Climbable", climbableUnifier,
|
||||
func(newValue bool) {
|
||||
view.requestChangeProperties(func(prop *texture.Properties) {
|
||||
prop.Climbable = 0
|
||||
if newValue {
|
||||
prop.Climbable = 1
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
values.RenderUnifiedCombo(readOnly, "Transparency Control", transparencyControlUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(index int) string { return texture.TransparencyControl(index).String() },
|
||||
len(texture.TransparencyControls()),
|
||||
func(newValue int) {
|
||||
view.requestChangeProperties(func(prop *texture.Properties) {
|
||||
prop.TransparencyControl = texture.TransparencyControl(newValue)
|
||||
})
|
||||
})
|
||||
|
||||
values.RenderUnifiedSliderInt(readOnly, "Animation Group", animationGroupUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string { return "%d" },
|
||||
0, 3,
|
||||
func(newValue int) {
|
||||
view.requestChangeProperties(func(prop *texture.Properties) {
|
||||
prop.AnimationGroup = byte(newValue)
|
||||
})
|
||||
})
|
||||
values.RenderUnifiedSliderInt(readOnly, "Animation Index", animationIndexUnifier,
|
||||
func(u values.Unifier) int { return u.Unified().(int) },
|
||||
func(value int) string { return "%d" },
|
||||
0, 3,
|
||||
func(newValue int) {
|
||||
view.requestChangeProperties(func(prop *texture.Properties) {
|
||||
prop.AnimationIndex = byte(newValue)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) renderTextureSample(label string, id resource.ID, sideLength float32, sizeID string) {
|
||||
if imgui.BeginChildV(label, imgui.Vec2{X: -1, Y: (128 + 7) * view.guiScale}, true, imgui.WindowFlagsNoScrollbar) {
|
||||
key := view.indexedResourceKey(id, view.model.currentIndex)
|
||||
render.TextureImage("Texture Bitmap", view.imageCache, key,
|
||||
imgui.Vec2{X: sideLength * view.guiScale, Y: sideLength * view.guiScale})
|
||||
|
||||
imgui.SameLine()
|
||||
imgui.BeginGroup()
|
||||
|
||||
tex, err := view.imageCache.Texture(key)
|
||||
|
||||
if imgui.Button("Clear") {
|
||||
view.requestClear(id, view.model.currentIndex, int(sideLength))
|
||||
}
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Import") {
|
||||
view.requestImport(id, view.model.currentIndex)
|
||||
}
|
||||
if err == nil {
|
||||
if imgui.Button("Export") {
|
||||
view.requestExport(id, view.model.currentIndex, sizeID)
|
||||
}
|
||||
if view.hasModCurrentBitmap() {
|
||||
imgui.SameLine()
|
||||
if imgui.Button("Remove") {
|
||||
view.requestSetBitmapData(id, view.model.currentIndex, nil)
|
||||
}
|
||||
}
|
||||
|
||||
width, height := tex.Size()
|
||||
imgui.Text(fmt.Sprintf("%d x %d px", int(width), int(height)))
|
||||
}
|
||||
|
||||
imgui.EndGroup()
|
||||
}
|
||||
imgui.EndChild()
|
||||
}
|
||||
|
||||
func (view *View) currentResourceKey() resource.Key {
|
||||
return view.indexedResourceKey(ids.LargeTextures, view.model.currentIndex)
|
||||
}
|
||||
|
||||
func (view *View) indexedResourceKey(id resource.ID, index int) resource.Key {
|
||||
info, _ := ids.Info(id)
|
||||
if info.List {
|
||||
return resource.KeyOf(id, resource.LangAny, index)
|
||||
}
|
||||
return resource.KeyOf(id.Plus(index), resource.LangAny, 0)
|
||||
}
|
||||
|
||||
func (view *View) textureTooltip(index int) string {
|
||||
key := resource.KeyOf(ids.TextureNames, resource.LangDefault, index)
|
||||
name, err := view.textCache.Text(key)
|
||||
suffix := ""
|
||||
if err == nil {
|
||||
suffix = ": " + name
|
||||
}
|
||||
return fmt.Sprintf("%3d", index) + suffix
|
||||
}
|
||||
|
||||
func (view *View) hasModCurrentBitmap() bool {
|
||||
key := view.currentResourceKey()
|
||||
return len(view.mod.ModifiedBlock(key.Lang, key.ID, key.Index)) > 0
|
||||
}
|
||||
|
||||
func (view *View) requestChangeProperties(modifier func(*texture.Properties)) {
|
||||
list := view.mod.TextureProperties()
|
||||
if view.model.currentIndex < len(list) {
|
||||
command := setTexturePropertiesCommand{
|
||||
model: &view.model,
|
||||
textureIndex: view.model.currentIndex,
|
||||
oldProperties: list[view.model.currentIndex],
|
||||
newProperties: list[view.model.currentIndex],
|
||||
}
|
||||
modifier(&command.newProperties)
|
||||
view.commander.Queue(command)
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) requestSetTextureName(value string) {
|
||||
view.requestSetTextureText(ids.TextureNames, value)
|
||||
}
|
||||
|
||||
func (view *View) requestSetTextureUsage(value string) {
|
||||
view.requestSetTextureText(ids.TextureUsages, value)
|
||||
}
|
||||
|
||||
func (view *View) requestSetTextureText(id resource.ID, newValue string) {
|
||||
key := resource.KeyOf(id, view.model.currentLang, view.model.currentIndex)
|
||||
oldValue, _ := view.textCache.Text(key)
|
||||
|
||||
if oldValue != newValue {
|
||||
command := setTextureTextCommand{
|
||||
model: &view.model,
|
||||
key: key,
|
||||
oldData: view.cp.Encode(oldValue),
|
||||
newData: view.cp.Encode(text.Blocked(newValue)[0]),
|
||||
}
|
||||
view.commander.Queue(command)
|
||||
}
|
||||
}
|
||||
|
||||
func (view *View) requestExport(id resource.ID, index int, sizeID string) {
|
||||
key := view.indexedResourceKey(id, index)
|
||||
tex, err := view.imageCache.Texture(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
palette, err := view.paletteCache.Palette(0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rawPalette := palette.Palette()
|
||||
filename := fmt.Sprintf("Texture_%03d_%s.png", index, sizeID)
|
||||
width, height := tex.Size()
|
||||
bmp := bitmap.Bitmap{
|
||||
Header: bitmap.Header{
|
||||
Width: int16(width),
|
||||
Height: int16(height),
|
||||
},
|
||||
Pixels: tex.PixelData(),
|
||||
Palette: &rawPalette,
|
||||
}
|
||||
|
||||
external.ExportImage(view.modalStateMachine, filename, bmp)
|
||||
}
|
||||
|
||||
func (view *View) requestImport(id resource.ID, index int) {
|
||||
paletteRetriever := func() (bitmap.Palette, error) {
|
||||
palette, err := view.paletteCache.Palette(0)
|
||||
if err != nil {
|
||||
return bitmap.Palette{}, err
|
||||
}
|
||||
return palette.Palette(), nil
|
||||
}
|
||||
|
||||
external.ImportImage(view.modalStateMachine, paletteRetriever, func(bmp bitmap.Bitmap) {
|
||||
view.requestSetBitmap(id, index, bmp)
|
||||
})
|
||||
}
|
||||
|
||||
func (view *View) requestClear(id resource.ID, index int, sideLength int) {
|
||||
bmp := bitmap.Bitmap{
|
||||
Header: bitmap.Header{
|
||||
Width: int16(sideLength),
|
||||
Height: int16(sideLength),
|
||||
},
|
||||
Pixels: []byte{0x00},
|
||||
}
|
||||
view.requestSetBitmap(id, index, bmp)
|
||||
}
|
||||
|
||||
func (view *View) requestSetBitmap(id resource.ID, index int, bmp bitmap.Bitmap) {
|
||||
highestBitShift := func(value int16) (result byte) {
|
||||
if value != 0 {
|
||||
for (value >> result) != 1 {
|
||||
result++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
bmp.Header.Flags = 0
|
||||
bmp.Header.Type = bitmap.TypeFlat8Bit
|
||||
bmp.Header.WidthFactor = highestBitShift(bmp.Header.Width)
|
||||
bmp.Header.HeightFactor = highestBitShift(bmp.Header.Height)
|
||||
bmp.Header.Stride = uint16(bmp.Header.Width)
|
||||
data := bitmap.Encode(&bmp, 0)
|
||||
view.requestSetBitmapData(id, index, data)
|
||||
}
|
||||
|
||||
func (view *View) requestSetBitmapData(id resource.ID, index int, newData []byte) {
|
||||
resourceKey := view.indexedResourceKey(id, index)
|
||||
command := setTextureBitmapCommand{
|
||||
model: &view.model,
|
||||
id: id,
|
||||
textureIndex: index,
|
||||
oldData: view.mod.ModifiedBlock(resource.LangAny, resourceKey.ID, resourceKey.Index),
|
||||
newData: newData,
|
||||
}
|
||||
view.commander.Queue(command)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package textures
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
)
|
||||
|
||||
type viewModel struct {
|
||||
windowOpen bool
|
||||
restoreFocus bool
|
||||
|
||||
currentLang resource.Language
|
||||
currentIndex int
|
||||
}
|
||||
|
||||
func freshViewModel() viewModel {
|
||||
return viewModel{
|
||||
currentIndex: 0,
|
||||
currentLang: resource.LangDefault,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package values
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/interpreters"
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
)
|
||||
|
||||
// StandardSimplifier returns a simplifier with common property controls.
|
||||
func StandardSimplifier(readOnly bool, fullKey string, unifier Unifier,
|
||||
updater func(func(uint32) uint32), objTypeRenderer ObjectTypeControlRenderer) *interpreters.Simplifier {
|
||||
keys := strings.Split(fullKey, ".")
|
||||
key := keys[len(keys)-1]
|
||||
label := key + "###" + fullKey
|
||||
|
||||
simplifier := interpreters.NewSimplifier(func(minValue, maxValue int64, formatter interpreters.RawValueFormatter) {
|
||||
RenderUnifiedSliderInt(readOnly, label, unifier,
|
||||
func(u Unifier) int {
|
||||
unifiedValue := u.Unified().(int32)
|
||||
if (minValue == -1) && (maxValue == 0x7FFF) {
|
||||
unifiedValue = int32(int16(unifiedValue))
|
||||
}
|
||||
return int(unifiedValue)
|
||||
},
|
||||
func(value int) string {
|
||||
result := formatter(value)
|
||||
if len(result) == 0 {
|
||||
result = "%d"
|
||||
} else {
|
||||
result += " - raw: %d"
|
||||
}
|
||||
return result
|
||||
},
|
||||
int(minValue), int(maxValue),
|
||||
func(newValue int) {
|
||||
updater(func(oldValue uint32) uint32 { return uint32(newValue) })
|
||||
})
|
||||
})
|
||||
|
||||
simplifier.SetEnumValueHandler(func(enumValues map[uint32]string) {
|
||||
valueKeys := make([]uint32, 0, len(enumValues))
|
||||
for valueKey := range enumValues {
|
||||
valueKeys = append(valueKeys, valueKey)
|
||||
}
|
||||
sort.Slice(valueKeys, func(indexA, indexB int) bool { return valueKeys[indexA] < valueKeys[indexB] })
|
||||
|
||||
RenderUnifiedCombo(readOnly, label, unifier,
|
||||
func(u Unifier) int {
|
||||
unifiedValue := uint32(u.Unified().(int32))
|
||||
for index, valueKey := range valueKeys {
|
||||
if valueKey == unifiedValue {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
},
|
||||
func(index int) string {
|
||||
if index < 0 {
|
||||
return ""
|
||||
}
|
||||
return enumValues[valueKeys[index]]
|
||||
},
|
||||
len(valueKeys),
|
||||
func(newIndex int) {
|
||||
updater(func(oldValue uint32) uint32 { return valueKeys[newIndex] })
|
||||
})
|
||||
})
|
||||
|
||||
simplifier.SetBitfieldHandler(func(maskNames map[uint32]string) {
|
||||
masks := make([]uint32, 0, len(maskNames))
|
||||
for mask := range maskNames {
|
||||
masks = append(masks, mask)
|
||||
}
|
||||
sort.Slice(masks, func(indexA, indexB int) bool { return masks[indexA] < masks[indexB] })
|
||||
|
||||
addMaskedItem := func(mask uint32) {
|
||||
maxValue := mask
|
||||
shift := 0
|
||||
maskedLabel := key + "." + maskNames[mask] + "###" + label + "-" + maskNames[mask]
|
||||
|
||||
for (maxValue & 1) == 0 {
|
||||
shift++
|
||||
maxValue >>= 1
|
||||
}
|
||||
|
||||
if maxValue == 1 {
|
||||
booleanUnifier := NewUnifier()
|
||||
if unifier.IsUnique() {
|
||||
booleanUnifier.Add((uint32(unifier.Unified().(int32)) & mask) != 0)
|
||||
}
|
||||
RenderUnifiedCheckboxCombo(readOnly, maskedLabel, booleanUnifier,
|
||||
func(newValue bool) {
|
||||
updater(func(oldValue uint32) uint32 {
|
||||
result := oldValue & ^mask
|
||||
if newValue {
|
||||
result |= mask
|
||||
}
|
||||
return result
|
||||
})
|
||||
})
|
||||
} else {
|
||||
RenderUnifiedSliderInt(readOnly, maskedLabel, unifier,
|
||||
func(u Unifier) int { return int((uint32(u.Unified().(int32)) & mask) >> uint32(shift)) },
|
||||
func(value int) string { return "%d" },
|
||||
0, int(maxValue),
|
||||
func(newValue int) {
|
||||
updater(func(oldValue uint32) uint32 {
|
||||
return (oldValue & ^mask) | (uint32(newValue) << uint32(shift))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, mask := range masks {
|
||||
addMaskedItem(mask)
|
||||
}
|
||||
})
|
||||
|
||||
simplifier.SetRotationHandler(func(minValue, maxValue int64) {
|
||||
RenderUnifiedRotation(readOnly, label, unifier,
|
||||
int(minValue), int(maxValue),
|
||||
RotationInfo{
|
||||
// This code is specialized for Hacker-Yaw. To re-use it for other rotations, details must change.
|
||||
Horizontal: true,
|
||||
Positive: true,
|
||||
Clockwise: false,
|
||||
},
|
||||
func(newValue int) {
|
||||
updater(func(oldValue uint32) uint32 { return uint32(newValue) })
|
||||
})
|
||||
})
|
||||
|
||||
simplifier.SetSpecialHandler("ObjectTriple", func() {
|
||||
var classNames [object.ClassCount]string
|
||||
for index, class := range object.Classes() {
|
||||
classNames[index] = class.String()
|
||||
}
|
||||
tripleResolver := func(u Unifier) object.Triple { return object.TripleFromInt(int(u.Unified().(int32))) }
|
||||
RenderUnifiedCombo(readOnly, key+"-Class###"+fullKey+"-Class", unifier,
|
||||
func(u Unifier) int {
|
||||
triple := tripleResolver(u)
|
||||
return int(triple.Class)
|
||||
},
|
||||
func(value int) string { return fmt.Sprintf("%2d: %v", value, object.Class(value)) },
|
||||
object.ClassCount,
|
||||
func(newValue int) {
|
||||
triple := object.TripleFrom(newValue, 0, 0)
|
||||
updater(func(oldValue uint32) uint32 { return uint32(triple.Int()) })
|
||||
})
|
||||
|
||||
objTypeRenderer.Render(readOnly, key+"###"+fullKey+"-Type", unifier, unifier,
|
||||
tripleResolver,
|
||||
func(newValue object.Triple) {
|
||||
updater(func(oldValue uint32) uint32 { return uint32(newValue.Int()) })
|
||||
})
|
||||
})
|
||||
|
||||
simplifier.SetSpecialHandler("Mistake", func() {})
|
||||
simplifier.SetSpecialHandler("Ignored", func() {})
|
||||
simplifier.SetSpecialHandler("Unknown", func() {})
|
||||
simplifier.SetSpecialHandler("Internal", func() {})
|
||||
|
||||
return simplifier
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package values
|
||||
|
||||
import (
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/object"
|
||||
"github.com/inkyblackness/hacked/ss1/content/text"
|
||||
"github.com/inkyblackness/hacked/ss1/resource"
|
||||
"github.com/inkyblackness/hacked/ss1/world/ids"
|
||||
)
|
||||
|
||||
// ObjectTypeControlRenderer renders a control to select an object.
|
||||
type ObjectTypeControlRenderer struct {
|
||||
// Meta describes the layout of classes, subclasses, and types.
|
||||
Meta object.PropertiesTable
|
||||
// TextCache is used to retrieve the name of a type.
|
||||
TextCache *text.Cache
|
||||
}
|
||||
|
||||
// Render creates the controls according to the given parameters.
|
||||
func (renderer ObjectTypeControlRenderer) Render(readOnly bool, label string,
|
||||
classUnifier Unifier, typeUnifier Unifier,
|
||||
tripleResolver func(Unifier) object.Triple,
|
||||
changeHandler func(object.Triple)) {
|
||||
switch {
|
||||
case classUnifier.IsUnique():
|
||||
class := tripleResolver(classUnifier).Class
|
||||
triples := renderer.Meta.TriplesInClass(class)
|
||||
selectedIndex := -1
|
||||
if typeUnifier.IsUnique() {
|
||||
triple := tripleResolver(typeUnifier)
|
||||
for index, availableTriple := range triples {
|
||||
if availableTriple == triple {
|
||||
selectedIndex = index
|
||||
}
|
||||
}
|
||||
if selectedIndex < 0 {
|
||||
selectedIndex = len(triples)
|
||||
triples = append(triples, triple)
|
||||
}
|
||||
}
|
||||
RenderUnifiedCombo(readOnly, label, typeUnifier,
|
||||
func(u Unifier) int { return selectedIndex },
|
||||
func(value int) string {
|
||||
triple := triples[value]
|
||||
return renderer.tripleName(triple)
|
||||
},
|
||||
len(triples),
|
||||
func(newValue int) {
|
||||
triple := triples[newValue]
|
||||
changeHandler(triple)
|
||||
})
|
||||
case !classUnifier.IsEmpty():
|
||||
imgui.LabelText(label, "(multiple classes)")
|
||||
default:
|
||||
imgui.LabelText(label, "")
|
||||
}
|
||||
}
|
||||
|
||||
func (renderer ObjectTypeControlRenderer) tripleName(triple object.Triple) string {
|
||||
suffix := "???"
|
||||
linearIndex := renderer.Meta.TripleIndex(triple)
|
||||
if (linearIndex >= 0) && (renderer.TextCache != nil) {
|
||||
key := resource.KeyOf(ids.ObjectLongNames, resource.LangDefault, linearIndex)
|
||||
objName, err := renderer.TextCache.Text(key)
|
||||
if err == nil {
|
||||
suffix = objName
|
||||
}
|
||||
}
|
||||
return triple.String() + ": " + suffix
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package values
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"github.com/inkyblackness/imgui-go/v3"
|
||||
|
||||
"github.com/inkyblackness/hacked/ui/gui"
|
||||
)
|
||||
|
||||
const (
|
||||
hintMultiple = "(multiple)"
|
||||
hintYes = "Yes"
|
||||
hintNo = "No"
|
||||
)
|
||||
|
||||
// RenderUnifiedSliderInt renders a control for an unified integer value.
|
||||
func RenderUnifiedSliderInt(readOnly bool, label string, unifier Unifier,
|
||||
intConverter func(Unifier) int, formatter func(int) string, min, max int, changeHandler func(int)) {
|
||||
var labelValue string
|
||||
var selectedString string
|
||||
selectedValue := -1
|
||||
if unifier.IsUnique() {
|
||||
selectedValue = intConverter(unifier)
|
||||
selectedString = formatter(selectedValue)
|
||||
labelValue = fmt.Sprintf(selectedString, selectedValue)
|
||||
} else if !unifier.IsEmpty() {
|
||||
selectedString = hintMultiple
|
||||
labelValue = selectedString
|
||||
}
|
||||
if readOnly {
|
||||
imgui.LabelText(label, labelValue)
|
||||
} else if gui.StepSliderIntV(label, &selectedValue, min, max, selectedString) {
|
||||
changeHandler(selectedValue)
|
||||
}
|
||||
}
|
||||
|
||||
// RenderUnifiedCombo renders a control for a unified enumeration value.
|
||||
func RenderUnifiedCombo(readOnly bool, label string, unifier Unifier,
|
||||
intConverter func(Unifier) int, formatter func(int) string, count int, changeHandler func(int)) {
|
||||
var selectedString string
|
||||
selectedIndex := -1
|
||||
if unifier.IsUnique() {
|
||||
selectedIndex = intConverter(unifier)
|
||||
selectedString = formatter(selectedIndex)
|
||||
} else if !unifier.IsEmpty() {
|
||||
selectedString = hintMultiple
|
||||
}
|
||||
if readOnly {
|
||||
imgui.LabelText(label, selectedString)
|
||||
} else if imgui.BeginCombo(label, selectedString) {
|
||||
for i := 0; i < count; i++ {
|
||||
entryString := formatter(i)
|
||||
|
||||
if imgui.SelectableV(entryString, i == selectedIndex, 0, imgui.Vec2{}) {
|
||||
changeHandler(i)
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
}
|
||||
|
||||
// RenderUnifiedCheckboxCombo renders a control for a unified boolean value.
|
||||
func RenderUnifiedCheckboxCombo(readOnly bool, label string, unifier Unifier, changeHandler func(bool)) {
|
||||
selectedString := ""
|
||||
if unifier.IsUnique() {
|
||||
selectedValue := unifier.Unified().(bool)
|
||||
if selectedValue {
|
||||
selectedString = hintYes
|
||||
} else {
|
||||
selectedString = hintNo
|
||||
}
|
||||
} else if !unifier.IsEmpty() {
|
||||
selectedString = hintMultiple
|
||||
}
|
||||
if readOnly {
|
||||
imgui.LabelText(label, selectedString)
|
||||
} else if imgui.BeginCombo(label, selectedString) {
|
||||
for i, text := range []string{hintNo, hintYes} {
|
||||
if imgui.SelectableV(text, text == selectedString, 0, imgui.Vec2{}) {
|
||||
changeHandler(i != 0)
|
||||
}
|
||||
}
|
||||
imgui.EndCombo()
|
||||
}
|
||||
}
|
||||
|
||||
// RotationInfo describes how the rotation is aligned in its zero position.
|
||||
type RotationInfo struct {
|
||||
Horizontal bool
|
||||
Positive bool
|
||||
Clockwise bool
|
||||
}
|
||||
|
||||
// RenderUnifiedRotation renders a control for rotation value.
|
||||
func RenderUnifiedRotation(readOnly bool, label string, unifier Unifier,
|
||||
min, max int, info RotationInfo, changeHandler func(int)) {
|
||||
calcValuePercent := func(value int) float64 {
|
||||
valueRange := (max - min) + 1
|
||||
return float64(value) / float64(valueRange)
|
||||
}
|
||||
RenderUnifiedSliderInt(readOnly, label, unifier,
|
||||
func(u Unifier) int {
|
||||
unifiedValue := u.Unified().(int32)
|
||||
return int(unifiedValue)
|
||||
},
|
||||
func(value int) string {
|
||||
valuePercent := calcValuePercent(value)
|
||||
result := fmt.Sprintf("%3.02f° - raw: %%d", valuePercent*360.0)
|
||||
return result
|
||||
},
|
||||
min, max,
|
||||
changeHandler)
|
||||
|
||||
if (imgui.IsItemFocused() || imgui.IsItemActive() || imgui.IsItemHovered()) && unifier.IsUnique() {
|
||||
valuePercent := calcValuePercent(int(unifier.Unified().(int32)))
|
||||
valueRadian := valuePercent * (math.Pi * 2)
|
||||
|
||||
lineColor := imgui.Packed(color.RGBA{R: 0x21, G: 0xFF, B: 0x43, A: 0xFF}) // color copied from style.
|
||||
lineThickness := float32(4.0)
|
||||
imgui.BeginTooltip()
|
||||
imgui.Dummy(imgui.Vec2{X: imgui.TextLineHeightWithSpacing() * 2, Y: imgui.TextLineHeightWithSpacing() * 2})
|
||||
dl := imgui.WindowDrawList()
|
||||
winTopLeft := imgui.WindowPos()
|
||||
winSize := imgui.WindowSize()
|
||||
center := winTopLeft.Plus(winSize.Times(0.5))
|
||||
circleRadius := ((winSize.X / 2) * 5) / 6
|
||||
dl.AddCircleV(center, circleRadius, lineColor, 100, lineThickness)
|
||||
|
||||
targetX := 0.0
|
||||
targetY := 0.0
|
||||
if info.Horizontal {
|
||||
targetX = 1.0
|
||||
if !info.Positive {
|
||||
targetX = -1.0
|
||||
}
|
||||
} else {
|
||||
targetY = -1.0
|
||||
if !info.Positive {
|
||||
targetY = 1.0
|
||||
}
|
||||
}
|
||||
var target imgui.Vec2
|
||||
if info.Clockwise {
|
||||
target.X = center.X - float32((float64(circleRadius)*targetX)*math.Cos(valueRadian)+(float64(circleRadius)*targetY)*math.Sin(valueRadian))
|
||||
target.Y = center.Y + float32((float64(circleRadius)*targetY)*math.Cos(valueRadian)-(float64(circleRadius)*targetX)*math.Sin(valueRadian))
|
||||
} else {
|
||||
target.X = center.X + float32((float64(circleRadius)*targetX)*math.Cos(valueRadian)-(float64(circleRadius)*targetY)*math.Sin(valueRadian))
|
||||
target.Y = center.Y - float32((float64(circleRadius)*targetY)*math.Cos(valueRadian)+(float64(circleRadius)*targetX)*math.Sin(valueRadian))
|
||||
}
|
||||
dl.AddLineV(center, target, lineColor, lineThickness)
|
||||
imgui.EndTooltip()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package values
|
||||
|
||||
// Unifier can be used to verify whether a list of values has equal entries.
|
||||
type Unifier struct {
|
||||
state unifierState
|
||||
}
|
||||
|
||||
// NewUnifier returns a new instance.
|
||||
func NewUnifier() Unifier {
|
||||
return Unifier{state: unifierInitState{}}
|
||||
}
|
||||
|
||||
// UnifierFor returns a unifier for a single value.
|
||||
func UnifierFor(value interface{}) Unifier {
|
||||
unifier := NewUnifier()
|
||||
unifier.Add(value)
|
||||
return unifier
|
||||
}
|
||||
|
||||
// Add unifies the given value to the current state.
|
||||
// The given value must be comparable.
|
||||
func (u *Unifier) Add(value interface{}) {
|
||||
u.state = u.state.add(value)
|
||||
}
|
||||
|
||||
// Unified returns the result of the unification.
|
||||
// If all values that were added to the unifier were equal, then the first
|
||||
// added value will be returned. Otherwise, nil will be returned.
|
||||
func (u *Unifier) Unified() interface{} {
|
||||
return u.state.unified()
|
||||
}
|
||||
|
||||
// IsUnique returns true if the unifier has received only equal values.
|
||||
func (u Unifier) IsUnique() bool {
|
||||
return u.state.isUnique()
|
||||
}
|
||||
|
||||
// IsEmpty returns true if the unifier has not received any value.
|
||||
func (u Unifier) IsEmpty() bool {
|
||||
return (u.state == nil) || u.state.isEmpty()
|
||||
}
|
||||
|
||||
type unifierState interface {
|
||||
add(value interface{}) unifierState
|
||||
unified() interface{}
|
||||
isUnique() bool
|
||||
isEmpty() bool
|
||||
}
|
||||
|
||||
type unifierInitState struct{}
|
||||
|
||||
func (state unifierInitState) add(value interface{}) unifierState {
|
||||
return unifierMatchedState{value: value}
|
||||
}
|
||||
|
||||
func (state unifierInitState) unified() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (state unifierInitState) isUnique() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (state unifierInitState) isEmpty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type unifierMatchedState struct {
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (state unifierMatchedState) add(value interface{}) unifierState {
|
||||
if state.value == value {
|
||||
return state
|
||||
}
|
||||
return unifierMismatchedState{}
|
||||
}
|
||||
|
||||
func (state unifierMatchedState) unified() interface{} {
|
||||
return state.value
|
||||
}
|
||||
|
||||
func (state unifierMatchedState) isUnique() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (state unifierMatchedState) isEmpty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type unifierMismatchedState struct{}
|
||||
|
||||
func (state unifierMismatchedState) add(value interface{}) unifierState {
|
||||
return state
|
||||
}
|
||||
|
||||
func (state unifierMismatchedState) unified() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (state unifierMismatchedState) isUnique() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (state unifierMismatchedState) isEmpty() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package values_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/editor/values"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUnifier_Integers(t *testing.T) {
|
||||
tt := []struct {
|
||||
values []int
|
||||
expectedValue interface{}
|
||||
expectedUnified bool
|
||||
}{
|
||||
{[]int{1, 1, 1}, 1, true},
|
||||
{[]int{}, nil, false},
|
||||
{[]int{1, 2, 3}, nil, false},
|
||||
}
|
||||
|
||||
for tn, tc := range tt {
|
||||
u := values.NewUnifier()
|
||||
for _, value := range tc.values {
|
||||
u.Add(value)
|
||||
}
|
||||
resultValue := u.Unified()
|
||||
resultUnified := u.IsUnique()
|
||||
assert.Equal(t, tc.expectedValue, resultValue, fmt.Sprintf("Wrong result for test number %d", tn))
|
||||
assert.Equal(t, tc.expectedUnified, resultUnified, fmt.Sprintf("Wrong unified for test number %d", tn))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnifier_Strings(t *testing.T) {
|
||||
tt := []struct {
|
||||
values []string
|
||||
expectedValue interface{}
|
||||
expectedUnified bool
|
||||
}{
|
||||
{[]string{"a", "a", "a"}, "a", true},
|
||||
{[]string{}, nil, false},
|
||||
{[]string{"a", "b", "c"}, nil, false},
|
||||
}
|
||||
|
||||
for tn, tc := range tt {
|
||||
u := values.NewUnifier()
|
||||
for _, value := range tc.values {
|
||||
u.Add(value)
|
||||
}
|
||||
resultValue := u.Unified()
|
||||
resultUnified := u.IsUnique()
|
||||
assert.Equal(t, tc.expectedValue, resultValue, fmt.Sprintf("Wrong result for test number %d", tn))
|
||||
assert.Equal(t, tc.expectedUnified, resultUnified, fmt.Sprintf("Wrong unified for test number %d", tn))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnifier_Types(t *testing.T) {
|
||||
type simpleType struct {
|
||||
value int
|
||||
text string
|
||||
}
|
||||
tt := []struct {
|
||||
values []simpleType
|
||||
expectedValue interface{}
|
||||
expectedUnified bool
|
||||
}{
|
||||
{[]simpleType{{value: 1, text: "a"}, {value: 1, text: "a"}, {value: 1, text: "a"}}, simpleType{value: 1, text: "a"}, true},
|
||||
{[]simpleType{}, nil, false},
|
||||
{[]simpleType{{value: 1, text: "a"}, {value: 1, text: "a"}, {value: 2, text: "a"}}, nil, false},
|
||||
{[]simpleType{{value: 1, text: "a"}, {value: 1, text: "a"}, {value: 1, text: "b"}}, nil, false},
|
||||
}
|
||||
|
||||
for tn, tc := range tt {
|
||||
u := values.NewUnifier()
|
||||
for _, value := range tc.values {
|
||||
u.Add(value)
|
||||
}
|
||||
resultValue := u.Unified()
|
||||
resultUnified := u.IsUnique()
|
||||
assert.Equal(t, tc.expectedValue, resultValue, fmt.Sprintf("Wrong result for test number %d", tn))
|
||||
assert.Equal(t, tc.expectedUnified, resultUnified, fmt.Sprintf("Wrong unified for test number %d", tn))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user