Adding modding folder with a hacked build.

This commit is contained in:
ml
2026-09-26 08:53:08 +02:00
parent cdb960dc71
commit c91080aba7
755 changed files with 50424 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
* text=auto eol=lf
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
+5
View File
@@ -0,0 +1,5 @@
.idea/
_build/
imgui.ini
*.syso
+84
View File
@@ -0,0 +1,84 @@
run:
deadline: 10m
linters:
enable:
- asciicheck
- bodyclose
- deadcode
- depguard
- dogsled
- dupl
- durationcheck
- errcheck
- errorlint
- exportloopref
- forbidigo
- goconst
- gocritic
- godot
- godox
- goerr113
- gofmt
- goheader
- gomoddirectives
- gomodguard
- goprintffuncname
- gosimple
- govet
- importas
- ineffassign
- lll
- makezero
- misspell
- nilerr
- noctx
- nolintlint
- predeclared
- promlinter
- revive
- rowserrcheck
- sqlclosecheck
- staticcheck
- structcheck
- tagliatelle
- testpackage
- thelper
- tparallel
- typecheck
- unconvert
- unused
- varcheck
- wastedassign
- whitespace
# linters are disabled if their majority of issues is considered false-positive (intended code)
# and the remaining issues (if existing) aren't worth it.
disable:
- gochecknoglobals
- gochecknoinits
- goimports # disabled because of so many false-positives with "imgui-go"
- gosec
- maligned
- nakedret
- prealloc
- unparam
- wsl
- funlen
- gocyclo # disabled for UI code
- nestif # disabled for UI code
- gocognit
- stylecheck # enable again for doing all the package documentation.
- gomnd # too many false positives with constants themselves.
issues:
exclude:
# too often for OpenGL constants
- don't use ALL_CAPS in Go names
exclude-use-default: false # disable filtering of defaults for better zero-issue policy
max-per-linter: 0 # disable limit; report all issues of a linter
max-same-issues: 0 # disable limit; report all issues of the same issue
linters-settings:
lll:
line-length: 160
+38
View File
@@ -0,0 +1,38 @@
## Contributing Guidelines
Thank you for considering to contribute to this application!
The following text lists guidelines for contributions.
These guidelines don't have legal status, so use them as a reference and common sense - and feel free to update them as well!
### "I just want to know..."
For questions, or general usage-information about HackEd, please refer to the [systemshock.org](https://systemshock.org) forums (`Engineering` subforum for modding in general, `Deck 13` for source development, including engine code), or refer to the [Wiki](https://github.com/inkyblackness/hacked/wiki).
You might also find information on the data files in the [ss-specs](https://github.com/inkyblackness/ss-specs) project.
### Scope
This is an editor for mods and fan-missions of the video game System Shock (1994, and compatible), written in Go.
If there are extensions to the data files, please be sure that these were discussed and/or documented in `ss-specs`. Usability features are always welcome, I only ask to give them a second thought if there might be a more generic solution (i.e., don't immediately jump to the implementation of "I need X", rather think how a slightly different feature might both give you X and also Y in even a simpler form...)
Please also consider engine compatibility. If there are engine features not supported by all (especially the classic version), please make sure these are indicated to the user. Also refer to the documentation on the Wiki for such cases.
### Code Style
Please make sure code is formatted according to `go fmt`, and use the following linter: [golangci-lint](https://github.com/golangci/golangci-lint).
> If there are linter errors that you didn't introduce, you don't have to clean them up - I might have missed them and will be handling them separately.
### Testing Style
The code under `ss1` should be well tested, please try to keep up coverage. The code under `editor` is tricky to test - if you figure out how to test ImGui based stuff, I'd be happy for a pull request! At least utilities should have tests.
> I am aiming for a coverage of over 90% in there.
For testing use either the vanilla `testing` package, or augment it with the `github.com/stretchr/testify` framework. See existing examples.
+27
View File
@@ -0,0 +1,27 @@
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.
+28
View File
@@ -0,0 +1,28 @@
# InkyBlackness HackEd
HackEd is the main graphical editor application as part of the [InkyBlackness](https://inkyblackness.github.io) project, written in [Go](http://golang.org/). It allows editing of game files of the original System Shock (1994 and compatible) to create mods and fan-missions.
This application is the successor of the `deck` package of InkyBlackness, which provided the first public editor, even before the source release of the original engine. The rewrite was necessary due to several factors.
The [releases page](https://github.com/inkyblackness/hacked/releases) provides the latest binaries for both MS Windows and Linux.
## Documentation
Usage of the editor is documented on its [Wiki](https://github.com/inkyblackness/hacked/wiki) page.
Answers and further help about modding can be furthermore found on the [systemshock.org](https://systemshock.org) forums, particularly the `Engineering` subforum.
## Screenshots
Level editing details:
![v1.0.0 Level Editing](https://user-images.githubusercontent.com/1508944/43354141-b8c536b0-9246-11e8-8014-c306be4ab1bb.png)
Media editing details:
![v1.0.0 Media Editing](https://user-images.githubusercontent.com/1508944/43354142-c3035742-9246-11e8-86d8-31cf62b7ca1b.png)
4K glory showing all the windows:
![v1.0.0 4K Glory](https://user-images.githubusercontent.com/1508944/43354138-a4676cc4-9246-11e8-883f-46ddde4aba25.png)
## License
The project is available under the terms of the **New BSD License** (see LICENSE file).
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
type="win32"
name="InkyBlackness.hacked"
version="§MAJOR.§MINOR.§PATCH.0"
/>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

@@ -0,0 +1,3 @@
100 ICON "hacked.ico"
100 24 "hacked.exe.manifest"
GLFW_ICON ICON "hacked.ico"
+17
View File
@@ -0,0 +1,17 @@
# InkyBlackness - HackEd
This package contains the public distribution of the [InkyBlackness](https://inkyblackness.github.io) HackEd binary.
The executable is a modding tool for the DOS game [System Shock (1994)](http://en.wikipedia.org/wiki/System_Shock) and compatibles.
## Disclaimer
A general warning: Keep backups of your files!
This is an executables downloaded from the internet. If you are unsure what they do or how they work, refer to the sources [on GitHub](https://github.com/inkyblackness).
## Requirements
HackEd is a GUI editor for the resources, including a map editor.
The tool uses OpenGL 3.2 .
User documentation is available online here: https://github.com/inkyblackness/hacked/wiki
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
pushd $(dirname "${0}") >/dev/null
HACKED_BASE=$(pwd -L)
FOLDER_NAME=inkyblackness-hacked
echo "Cleaning output directories..."
rm -rf _build
rm main-res.syso
mkdir -p $HACKED_BASE/_build/linux/$FOLDER_NAME
mkdir -p $HACKED_BASE/_build/win/$FOLDER_NAME
echo "Determining version"
MAJOR=$(date +%Y)
MINOR=$(date +%m)
PATCH=$(date +%d)
VERSION=$(git describe --tags)
if [ $? -ne 0 ]; then
echo "Could not determine tag, defaulting to revision for version"
REV=$(git rev-parse --short HEAD)
VERSION="rev$REV"
else
VERSION_RAW=$(echo "$VERSION" | cut -d'-' -f 1 | cut -d'v' -f 2)
fi
EXTRA=$(echo "$VERSION" | cut -d'-' -f 2)
if [[ "$VERSION" == "$EXTRA" ]]; then
MAJOR=$(echo "$VERSION_RAW" | cut -d'.' -f 1)
MINOR=$(echo "$VERSION_RAW" | cut -d'.' -f 2)
PATCH=$(echo "$VERSION_RAW" | cut -d'.' -f 3)
fi
echo "Determined version: $VERSION ($MAJOR.$MINOR.$PATCH)"
echo "Preparing build resources"
mkdir -p $HACKED_BASE/_build/win_temp
cp $HACKED_BASE/_resources/build/win/* $HACKED_BASE/_build/win_temp
sed -i "s/§MAJOR/$MAJOR/g" $HACKED_BASE/_build/win_temp/hacked.exe.manifest
sed -i "s/§MINOR/$MINOR/g" $HACKED_BASE/_build/win_temp/hacked.exe.manifest
sed -i "s/§PATCH/$PATCH/g" $HACKED_BASE/_build/win_temp/hacked.exe.manifest
x86_64-w64-mingw32-windres -o main-res.syso $HACKED_BASE/_build/win_temp/hacked.rc
echo "Building executables..."
go build -gcflags "-d=checkptr=0" -ldflags "-X main.version=$VERSION" -a -o $HACKED_BASE/_build/linux/$FOLDER_NAME/hacked -trimpath $(pwd) .
GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CXX=x86_64-w64-mingw32-g++ CC=x86_64-w64-mingw32-gcc go build -gcflags "-d=checkptr=0" -ldflags "-X main.version=$VERSION -H=windowsgui" -a -o $HACKED_BASE/_build/win/$FOLDER_NAME/hacked.exe -trimpath $(pwd) .
echo "Copying distribution resources..."
for os in "linux" "win"; do
packageDir=$HACKED_BASE/_build/$os/$FOLDER_NAME
cp $HACKED_BASE/LICENSE $packageDir
cp -R $HACKED_BASE/_resources/dist/* $packageDir
done
for lib in "libgcc_s_seh-1.dll" "libstdc++-6.dll" "libwinpthread-1.dll"; do
for base in "/usr/x86_64-w64-mingw32/bin" "/usr/lib/gcc/x86_64-w64-mingw32/10-win32" "/usr/x86_64-w64-mingw32/lib"; do
if [ -e $base/$lib ]; then
cp $base/$lib $HACKED_BASE/_build/win/$FOLDER_NAME
fi
done
if [ ! -f $HACKED_BASE/_build/win/$FOLDER_NAME/$lib ]; then
echo " FAIL: file $lib is nowhere found"
fi
done
echo "Creating packages..."
cd $HACKED_BASE/_build/linux
tar -cvzf $HACKED_BASE/_build/$FOLDER_NAME-$VERSION.linux64.tgz ./$FOLDER_NAME
cd $HACKED_BASE/_build/win
zip -r $HACKED_BASE/_build/$FOLDER_NAME-$VERSION.win64.zip .
popd >/dev/null
+4
View File
@@ -0,0 +1,4 @@
package crash
// Handler is a function that tries to preserve some panic/stack info to the user.
var Handler = func(versionInfo string) {}
+42
View File
@@ -0,0 +1,42 @@
package crash
//#include <windows.h>
import "C"
import (
"fmt"
"runtime/debug"
"unsafe"
)
func init() {
Handler = windowsHandler
}
func windowsHandler(versionInfo string) {
if e := recover(); e != nil {
text := `There was a crash in the application. This is bad and I am sorry.
Perhaps you can make something with the details of the error below.
If you can reproduce this, please make a screenshot of this box and report it with details on the http://www.systemshock.org forums.
Thank you!
`
text += fmt.Sprintf("%s:\n%s", e, debug.Stack())
messageBox(text, "Something unexpected happened - "+versionInfo)
}
}
func messageBox(text, caption string) {
textArg, textFin := wrapString(text)
defer textFin()
captionArg, captionFin := wrapString(caption)
defer captionFin()
var hwnd unsafe.Pointer
C.MessageBoxExA((*C.struct_HWND__)(hwnd), textArg, captionArg, C.UINT(0), C.WORD(0))
}
func wrapString(value string) (wrapped *C.char, finisher func()) {
wrapped = C.CString(value)
finisher = func() { C.free(unsafe.Pointer(wrapped)) } // nolint: gas
return
}
+945
View File
@@ -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(),
}
}
+15
View File
@@ -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()
}
}
+70
View File
@@ -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()
}
}
+9
View File
@@ -0,0 +1,9 @@
package about
type viewModel struct {
windowOpen bool
}
func freshViewModel() viewModel {
return viewModel{}
}
+227
View File
@@ -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
}
+338
View File
@@ -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
}
+934
View File
@@ -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
}
+271
View File
@@ -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),
}
}
+4
View File
@@ -0,0 +1,4 @@
/*
Package editor and its sub-packages contain the editor-specifics of hacked.
*/
package editor
+9
View File
@@ -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
View File
@@ -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)
}
+32
View File
@@ -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) {
}
+81
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+34
View File
@@ -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) {
}
+83
View File
@@ -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
View File
@@ -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)"
)
+409
View File
@@ -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"
}
}
+124
View File
@@ -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)
}
})
}
+135
View File
@@ -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))
}
+179
View File
@@ -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)
})
}
+587
View File
@@ -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{}
}
}
+312
View File
@@ -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)
}
}
+198
View File
@@ -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()}
}
+255
View File
@@ -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
}
+551
View File
@@ -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,
}
}
+64
View File
@@ -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)
}
}
}
+488
View File
@@ -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()
}
+615
View File
@@ -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())
}
+26
View File
@@ -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
}
+710
View File
@@ -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) {
}
+246
View File
@@ -0,0 +1,246 @@
package project
import (
"fmt"
"github.com/inkyblackness/imgui-go/v3"
"github.com/inkyblackness/hacked/ss1/edit"
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
"github.com/inkyblackness/hacked/ss1/world"
"github.com/inkyblackness/hacked/ui/gui"
)
// View handles the project display.
type View struct {
service *edit.ProjectService
modalStateMachine gui.ModalStateMachine
guiScale float32
commander cmd.Registry
model viewModel
}
// NewView creates a new instance for the project display.
func NewView(service *edit.ProjectService, modalStateMachine gui.ModalStateMachine, guiScale float32, commander cmd.Registry) *View {
return &View{
service: service,
modalStateMachine: modalStateMachine,
guiScale: guiScale,
commander: commander,
model: freshViewModel(),
}
}
// WindowOpen returns the flag address, to be used with the main menu.
func (view *View) WindowOpen() *bool {
return &view.model.windowOpen
}
// Render requests to render the view.
func (view *View) Render() {
if view.model.restoreFocus {
imgui.SetNextWindowFocus()
view.model.restoreFocus = false
view.model.windowOpen = true
}
title := "Project"
saveStatus := view.service.SaveStatus()
if saveStatus.FilesModified > 0 {
title += fmt.Sprintf(" - %d file(s) pending save", saveStatus.FilesModified)
if saveStatus.SavePending && (saveStatus.SaveIn.Seconds() < 4) {
title += fmt.Sprintf(" - auto-save in %d", int(saveStatus.SaveIn.Seconds()))
}
}
if view.model.windowOpen {
imgui.SetNextWindowSizeV(imgui.Vec2{X: 400 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver)
if imgui.BeginV(title+"###Project", view.WindowOpen(), 0) {
view.renderContent()
}
imgui.End()
}
}
func (view *View) renderContent() {
imgui.Text("Mod Location")
imgui.PushStyleVarVec2(imgui.StyleVarWindowPadding, imgui.Vec2{X: 1, Y: 0})
imgui.BeginChildV("ModLocation", imgui.Vec2{X: -200*view.guiScale - 10*view.guiScale, Y: imgui.TextLineHeight() * 1.5}, true,
imgui.WindowFlagsNoScrollbar|imgui.WindowFlagsNoScrollWithMouse)
modPath := view.service.ModPath()
if len(modPath) > 0 {
imgui.Text(modPath)
} else {
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1.0, Y: 1.0, Z: 1.0, W: 0.5})
imgui.Text("(new mod)")
imgui.PopStyleColor()
}
imgui.EndChild()
imgui.PopStyleVar()
imgui.BeginGroup()
imgui.SameLine()
if imgui.ButtonV("Save", imgui.Vec2{X: 100 * view.guiScale, Y: 0}) {
view.StartSavingMod()
}
imgui.SameLine()
if imgui.ButtonV("Load...", imgui.Vec2{X: 100 * view.guiScale, Y: 0}) {
view.startLoadingMod()
}
imgui.EndGroup()
imgui.Text("Static World Data")
imgui.BeginChildV("ManifestEntries", imgui.Vec2{X: -100 * view.guiScale, Y: 0}, true, 0)
manifest := view.service.Mod().World()
entries := manifest.EntryCount()
for i := entries - 1; i >= 0; i-- {
entry, _ := manifest.Entry(i)
if imgui.SelectableV(entry.ID, view.model.selectedManifestEntry == i, 0, imgui.Vec2{}) {
view.model.selectedManifestEntry = i
}
}
imgui.EndChild()
imgui.SameLine()
imgui.BeginGroup()
if imgui.ButtonV("Add...", imgui.Vec2{X: -1, Y: 0}) {
view.startAddingManifestEntry()
}
if imgui.ButtonV("Up", imgui.Vec2{X: -1, Y: 0}) {
view.requestMoveManifestEntryUp()
}
if imgui.ButtonV("Down", imgui.Vec2{X: -1, Y: 0}) {
view.requestMoveManifestEntryDown()
}
if imgui.ButtonV("Remove", imgui.Vec2{X: -1, Y: 0}) {
view.requestRemoveManifestEntry()
}
imgui.EndGroup()
}
func (view *View) startLoadingMod() {
view.modalStateMachine.SetState(&loadModStartState{
machine: view.modalStateMachine,
view: view,
})
}
// StartSavingMod initiates to save the mod.
// It either opens the save-as dialog, or simply saves under the current folder.
func (view *View) StartSavingMod() {
if view.service.ModHasStorageLocation() {
err := view.service.SaveMod()
if err != nil {
view.handleSaveModFailure(err)
}
} else {
view.modalStateMachine.SetState(&saveModAsStartState{
machine: view.modalStateMachine,
view: view,
})
}
}
func (view *View) startAddingManifestEntry() {
view.modalStateMachine.SetState(&addManifestEntryStartState{
machine: view.modalStateMachine,
view: view,
})
}
func (view *View) requestMoveManifestEntryUp() {
manifest := view.service.Mod().World()
entries := manifest.EntryCount()
if (view.model.selectedManifestEntry >= 0) && (view.model.selectedManifestEntry < (entries - 1)) {
view.requestMoveManifestEntry(view.model.selectedManifestEntry+1, view.model.selectedManifestEntry)
}
}
func (view *View) requestMoveManifestEntryDown() {
if view.model.selectedManifestEntry > 0 {
view.requestMoveManifestEntry(view.model.selectedManifestEntry-1, view.model.selectedManifestEntry)
}
}
func (view *View) requestMoveManifestEntry(to, from int) {
_ = view.commander.Register(
cmd.Named("moveManifestEntry"),
cmd.Reverse(view.taskToRestoreFocus()),
cmd.Reverse(view.taskToSelectEntry(from)),
cmd.Nested(func() error { return view.service.MoveManifestEntry(to, from) }),
cmd.Forward(view.taskToSelectEntry(to)),
cmd.Forward(view.taskToRestoreFocus()),
)
}
func (view *View) tryAddManifestEntryFrom(names []string) error {
entry, err := world.NewManifestEntryFrom(names)
if err != nil {
return err
}
view.requestAddManifestEntry(entry)
return nil
}
func (view *View) requestAddManifestEntry(entry *world.ManifestEntry) {
at := view.model.selectedManifestEntry + 1
_ = view.commander.Register(
cmd.Named("addManifestEntry"),
cmd.Reverse(view.taskToRestoreFocus()),
cmd.Reverse(view.taskToSelectEntry(view.model.selectedManifestEntry)),
cmd.Nested(func() error { return view.service.AddManifestEntry(at, entry) }),
cmd.Forward(view.taskToSelectEntry(at)),
cmd.Forward(view.taskToRestoreFocus()),
)
}
func (view *View) requestRemoveManifestEntry() {
manifest := view.service.Mod().World()
at := view.model.selectedManifestEntry
if (at < 0) || (at >= manifest.EntryCount()) {
return
}
_ = view.commander.Register(
cmd.Named("removeManifestEntry"),
cmd.Reverse(view.taskToRestoreFocus()),
cmd.Reverse(view.taskToSelectEntry(view.model.selectedManifestEntry)),
cmd.Nested(func() error { return view.service.RemoveManifestEntry(at) }),
cmd.Forward(view.taskToSelectEntry(view.model.selectedManifestEntry-1)),
cmd.Forward(view.taskToRestoreFocus()),
)
}
func (view *View) tryLoadModFrom(names []string) error {
return view.service.TryLoadModFrom(names)
}
func (view *View) requestSaveMod(modPath string) {
err := view.service.SaveModUnder(modPath)
if err != nil {
view.handleSaveModFailure(err)
}
}
func (view *View) handleSaveModFailure(err error) {
view.modalStateMachine.SetState(&saveModFailedState{
machine: view.modalStateMachine,
view: view,
errorInfo: err.Error(),
})
}
func (view *View) taskToRestoreFocus() cmd.Task {
return func(modder world.Modder) error {
view.model.restoreFocus = true
return nil
}
}
func (view *View) taskToSelectEntry(index int) cmd.Task {
return func(modder world.Modder) error {
view.model.selectedManifestEntry = index
return nil
}
}
@@ -0,0 +1,14 @@
package project
type viewModel struct {
restoreFocus bool
windowOpen bool
selectedManifestEntry int
}
func freshViewModel() viewModel {
return viewModel{
windowOpen: false,
selectedManifestEntry: -1,
}
}
+15
View File
@@ -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)
}
+34
View File
@@ -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()
}
+143
View File
@@ -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
}
}
+18
View File
@@ -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),
}
}
+198
View File
@@ -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
}
}
+18
View File
@@ -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
}

Some files were not shown because too many files have changed in this diff Show More