1021 lines
34 KiB
Go
1021 lines
34 KiB
Go
package levels
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
mgl "github.com/go-gl/mathgl/mgl32"
|
|
"github.com/inkyblackness/hacked/ui/gui"
|
|
"github.com/inkyblackness/hacked/ui/opengl"
|
|
"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"
|
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
|
"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/ids"
|
|
)
|
|
|
|
const shouldRenderRotationView = false
|
|
|
|
// ObjectsView is for object properties.
|
|
type ObjectsView struct {
|
|
gameObjects *edit.GameObjectsService
|
|
levelSelection *edit.LevelSelectionService
|
|
editor *edit.LevelEditorService
|
|
varInfoProvider archive.GameVariableInfoProvider
|
|
textCache *text.Cache
|
|
textureCache *graphics.TextureCache
|
|
|
|
guiScale float32
|
|
registry cmd.Registry
|
|
|
|
textureRenderer *render.TextureRenderer
|
|
orientationView *render.OrientationView
|
|
|
|
model objectsViewModel
|
|
}
|
|
|
|
// NewObjectsView returns a new instance.
|
|
func NewObjectsView(gameObjects *edit.GameObjectsService,
|
|
editor *edit.LevelEditorService,
|
|
levelSelection *edit.LevelSelectionService,
|
|
varInfoProvider archive.GameVariableInfoProvider,
|
|
guiScale float32, textCache *text.Cache, textureCache *graphics.TextureCache,
|
|
registry cmd.Registry, gl opengl.OpenGL) *ObjectsView {
|
|
textureRenderer := render.NewTextureRenderer(gl)
|
|
viewMatrix := mgl.LookAt(0.35, 0.35, -1, 0, 0, 0, 1.0, 0.0, 0.0)
|
|
context := render.Context{
|
|
OpenGL: gl,
|
|
ViewMatrix: &viewMatrix,
|
|
ProjectionMatrix: mgl.Ortho(-0.5, 0.5, 0.5, -0.5, -10, 10.0),
|
|
}
|
|
// Set up an orientation view. The orientation parameter would match the
|
|
// map as seen from above, and the rotation to match how the objects rotate in world.
|
|
orientationView := render.NewOrientationView(context,
|
|
mgl.Ident4().
|
|
Mul4(mgl.HomogRotate3D(math.Pi/2, mgl.Vec3{0.0, 0.0, 1.0})).
|
|
Mul4(mgl.HomogRotate3D(math.Pi, mgl.Vec3{1.0, 0.0, 0.0})),
|
|
mgl.Vec3{1.0, 1.0, -1.0})
|
|
|
|
view := &ObjectsView{
|
|
gameObjects: gameObjects,
|
|
levelSelection: levelSelection,
|
|
editor: editor,
|
|
varInfoProvider: varInfoProvider,
|
|
textCache: textCache,
|
|
textureCache: textureCache,
|
|
|
|
guiScale: guiScale,
|
|
registry: registry,
|
|
|
|
textureRenderer: textureRenderer,
|
|
orientationView: orientationView,
|
|
|
|
model: freshObjectsViewModel(),
|
|
}
|
|
return view
|
|
}
|
|
|
|
// WindowOpen returns the flag address, to be used with the main menu.
|
|
func (view *ObjectsView) WindowOpen() *bool {
|
|
return &view.model.windowOpen
|
|
}
|
|
|
|
// Render renders the view.
|
|
func (view *ObjectsView) Render() {
|
|
if view.model.restoreFocus {
|
|
imgui.SetNextWindowFocus()
|
|
view.model.restoreFocus = false
|
|
view.model.windowOpen = true
|
|
}
|
|
if view.model.windowOpen {
|
|
lvl := view.editor.Level()
|
|
objects := view.editor.Objects()
|
|
imgui.SetNextWindowSizeV(imgui.Vec2{X: 400 * view.guiScale, Y: 500 * view.guiScale}, imgui.ConditionFirstUseEver)
|
|
title := fmt.Sprintf("Level Objects, %d selected", len(objects))
|
|
readOnly := view.editor.IsReadOnly()
|
|
if readOnly {
|
|
title += hintReadOnly
|
|
}
|
|
if imgui.BeginV(title+"###Level Objects", view.WindowOpen(), imgui.WindowFlagsHorizontalScrollbar|imgui.WindowFlagsAlwaysVerticalScrollbar) {
|
|
view.renderContent(lvl, objects, readOnly)
|
|
}
|
|
imgui.End()
|
|
}
|
|
}
|
|
|
|
func (view *ObjectsView) renderContent(lvl *level.Level, objects []*level.ObjectMainEntry, readOnly bool) {
|
|
classUnifier := values.NewUnifier()
|
|
typeUnifier := values.NewUnifier()
|
|
zUnifier := values.NewUnifier()
|
|
tileXUnifier := values.NewUnifier()
|
|
fineXUnifier := values.NewUnifier()
|
|
tileYUnifier := values.NewUnifier()
|
|
fineYUnifier := values.NewUnifier()
|
|
rotationXUnifier := values.NewUnifier()
|
|
rotationYUnifier := values.NewUnifier()
|
|
rotationZUnifier := values.NewUnifier()
|
|
hitpointsUnifier := values.NewUnifier()
|
|
|
|
for _, obj := range objects {
|
|
classUnifier.Add(object.TripleFrom(int(obj.Class), 0, 0))
|
|
typeUnifier.Add(obj.Triple())
|
|
zUnifier.Add(obj.Z)
|
|
tileXUnifier.Add(obj.X.Tile())
|
|
fineXUnifier.Add(obj.X.Fine())
|
|
tileYUnifier.Add(obj.Y.Tile())
|
|
fineYUnifier.Add(obj.Y.Fine())
|
|
rotationXUnifier.Add(int32(obj.XRotation))
|
|
rotationYUnifier.Add(int32(obj.YRotation))
|
|
rotationZUnifier.Add(int32(obj.ZRotation))
|
|
hitpointsUnifier.Add(obj.Hitpoints)
|
|
}
|
|
|
|
imgui.PushItemWidth(-150 * view.guiScale)
|
|
columns, rows, levelHeight := lvl.Size()
|
|
|
|
objectHeightFormatter := objectHeightFormatterFor(levelHeight)
|
|
|
|
if !readOnly {
|
|
classString := func(class object.Class) string {
|
|
active, capacity := lvl.ObjectClassStats(class)
|
|
if class == object.ClassSmallStuff {
|
|
// The engine backs up inventory items in the level during a level transition.
|
|
// For this it requires space. If there is none, bugs do appear.
|
|
// This limit could apply for several classes, yet only small stuff is being used.
|
|
practicalLimit := 0
|
|
if capacity > level.InventorySize {
|
|
practicalLimit = capacity - level.InventorySize
|
|
}
|
|
return fmt.Sprintf("%2d: %v -- %d/%d (%d)", int(class), class, active, practicalLimit, capacity)
|
|
}
|
|
return fmt.Sprintf("%2d: %v -- %d/%d", int(class), class, active, capacity)
|
|
}
|
|
if imgui.BeginCombo("New Object Class", classString(view.editor.NewObjectTriple().Class)) {
|
|
for _, class := range object.Classes() {
|
|
if imgui.SelectableV(classString(class), class == view.editor.NewObjectTriple().Class, 0, imgui.Vec2{}) {
|
|
view.editor.SetNewObjectTriple(object.TripleFrom(int(class), 0, 0))
|
|
}
|
|
}
|
|
imgui.EndCombo()
|
|
}
|
|
if imgui.BeginCombo("New Object Type", view.tripleName(view.editor.NewObjectTriple())) {
|
|
allTypes := view.gameObjects.AllProperties().TriplesInClass(view.editor.NewObjectTriple().Class)
|
|
bitmapInfo := view.gameObjects.BitmapInfo()
|
|
lineHeight := imgui.TextLineHeight()
|
|
iconSize := imgui.Vec2{X: lineHeight * view.guiScale, Y: lineHeight * view.guiScale}
|
|
for index, triple := range allTypes {
|
|
info := bitmapInfo[triple]
|
|
iconKey := resource.KeyOf(ids.ObjectBitmaps, resource.LangAny, info.Start+info.IconRecommendation)
|
|
imgui.PushIDInt(index)
|
|
imgui.BeginGroup()
|
|
render.TextureImage("Icon", view.textureCache, iconKey, iconSize)
|
|
imgui.SameLine()
|
|
if imgui.SelectableV(view.tripleName(triple), triple == view.editor.NewObjectTriple(), 0, imgui.Vec2{}) {
|
|
view.editor.SetNewObjectTriple(triple)
|
|
}
|
|
imgui.EndGroup()
|
|
imgui.PopID()
|
|
}
|
|
imgui.EndCombo()
|
|
}
|
|
if imgui.Button("Delete Selected") {
|
|
view.requestDeleteObjects()
|
|
}
|
|
imgui.Separator()
|
|
}
|
|
|
|
selectedIDs := view.levelSelection.CurrentSelectedObjects()
|
|
switch {
|
|
case len(selectedIDs) == 1:
|
|
imgui.LabelText("ID", fmt.Sprintf("%3d", int(selectedIDs[0])))
|
|
case len(selectedIDs) > 1:
|
|
imgui.LabelText("ID", "(multiple)")
|
|
default:
|
|
imgui.LabelText("ID", "")
|
|
}
|
|
|
|
objTypeRenderer := values.ObjectTypeControlRenderer{
|
|
Meta: view.gameObjects.AllProperties(),
|
|
TextCache: view.textCache,
|
|
}
|
|
objTypeRenderer.Render(readOnly, "Object Type", classUnifier, typeUnifier,
|
|
func(u values.Unifier) object.Triple { return u.Unified().(object.Triple) },
|
|
func(newValue object.Triple) {
|
|
view.requestChangeObjects("ChangeBaseType", func(entry *level.ObjectMainEntry) {
|
|
entry.Subclass = newValue.Subclass
|
|
entry.Type = newValue.Type
|
|
})
|
|
})
|
|
|
|
if imgui.TreeNodeV("Base Properties", imgui.TreeNodeFlagsDefaultOpen|imgui.TreeNodeFlagsFramed) {
|
|
values.RenderUnifiedSliderInt(readOnly, "Z", zUnifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(level.HeightUnit)) },
|
|
objectHeightFormatter,
|
|
0, 0xFF,
|
|
func(newValue int) {
|
|
view.requestChangeObjects("ChangeBaseZ", func(entry *level.ObjectMainEntry) { entry.Z = level.HeightUnit(newValue) })
|
|
})
|
|
values.RenderUnifiedSliderInt(readOnly, "Tile X", tileXUnifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(byte)) },
|
|
func(value int) string { return "%d" },
|
|
0, columns-1,
|
|
func(newValue int) {
|
|
view.requestChangeObjects("ChangeBaseTileX", func(entry *level.ObjectMainEntry) { entry.X = level.CoordinateAt(byte(newValue), entry.X.Fine()) })
|
|
})
|
|
values.RenderUnifiedSliderInt(readOnly, "Fine X", fineXUnifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(byte)) },
|
|
func(value int) string { return "%d" },
|
|
0, level.FineCoordinatesPerTileSide-1,
|
|
func(newValue int) {
|
|
view.requestChangeObjects("ChangeBaseFineX", func(entry *level.ObjectMainEntry) { entry.X = level.CoordinateAt(entry.X.Tile(), byte(newValue)) })
|
|
})
|
|
values.RenderUnifiedSliderInt(readOnly, "Tile Y", tileYUnifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(byte)) },
|
|
func(value int) string { return "%d" },
|
|
0, rows-1,
|
|
func(newValue int) {
|
|
view.requestChangeObjects("ChangeBaseTileY", func(entry *level.ObjectMainEntry) { entry.Y = level.CoordinateAt(byte(newValue), entry.Y.Fine()) })
|
|
})
|
|
values.RenderUnifiedSliderInt(readOnly, "Fine Y", fineYUnifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(byte)) },
|
|
func(value int) string { return "%d" },
|
|
0, level.FineCoordinatesPerTileSide-1,
|
|
func(newValue int) {
|
|
view.requestChangeObjects("ChangeFineY", func(entry *level.ObjectMainEntry) { entry.Y = level.CoordinateAt(entry.Y.Tile(), byte(newValue)) })
|
|
})
|
|
|
|
values.RenderUnifiedRotation(readOnly, "Rotation X", rotationXUnifier,
|
|
0, 0x0FF,
|
|
values.RotationInfo{Horizontal: true, Positive: false, Clockwise: false},
|
|
func(newValue int) {
|
|
view.requestChangeObjects("ChangeBaseRotationX", func(entry *level.ObjectMainEntry) { entry.XRotation = level.RotationUnit(newValue) })
|
|
})
|
|
values.RenderUnifiedRotation(readOnly, "Rotation Y", rotationYUnifier,
|
|
0, 0x0FF,
|
|
values.RotationInfo{Horizontal: false, Positive: true, Clockwise: true},
|
|
func(newValue int) {
|
|
view.requestChangeObjects("ChangeBaseRotationY", func(entry *level.ObjectMainEntry) { entry.YRotation = level.RotationUnit(newValue) })
|
|
})
|
|
values.RenderUnifiedRotation(readOnly, "Rotation Z", rotationZUnifier,
|
|
0, 0x0FF,
|
|
values.RotationInfo{Horizontal: false, Positive: false, Clockwise: true},
|
|
func(newValue int) {
|
|
view.requestChangeObjects("ChangeBaseRotationZ", func(entry *level.ObjectMainEntry) { entry.ZRotation = level.RotationUnit(newValue) })
|
|
})
|
|
|
|
if shouldRenderRotationView {
|
|
var rotation mgl.Vec3
|
|
if rotationXUnifier.IsUnique() {
|
|
rotation[0] = float32(rotationXUnifier.Unified().(int32)*360) / 256.0
|
|
}
|
|
if rotationYUnifier.IsUnique() {
|
|
rotation[1] = float32(rotationYUnifier.Unified().(int32)*360) / 256.0
|
|
}
|
|
if rotationZUnifier.IsUnique() {
|
|
rotation[2] = float32(rotationZUnifier.Unified().(int32)*360) / 256.0
|
|
}
|
|
view.textureRenderer.Render(func() { view.orientationView.Render(rotation) })
|
|
imgui.ImageV(gui.TextureIDForColorTexture(view.textureRenderer.Handle()),
|
|
imgui.Vec2{X: 200 * view.guiScale, Y: 200 * view.guiScale},
|
|
imgui.Vec2{X: 0.0, Y: 0.0}, imgui.Vec2{X: 1.0, Y: 1.0},
|
|
imgui.Vec4{X: 1, Y: 1, Z: 1, W: 1.0}, imgui.Vec4{X: 0, Y: 0, Z: 0, W: 0})
|
|
}
|
|
|
|
values.RenderUnifiedSliderInt(readOnly, "Hitpoints", hitpointsUnifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(int16)) },
|
|
func(value int) string { return "%d" },
|
|
0, 10000,
|
|
func(newValue int) {
|
|
view.requestChangeObjects("ChangeBaseHitpoints", func(entry *level.ObjectMainEntry) { entry.Hitpoints = int16(newValue) })
|
|
})
|
|
imgui.TreePop()
|
|
}
|
|
if imgui.TreeNodeV("Extra Properties", imgui.TreeNodeFlagsFramed) {
|
|
view.renderProperties(lvl, objects, readOnly,
|
|
func(obj *level.ObjectMainEntry) *interpreters.Instance { return lvl.ObjectExtraData(obj) })
|
|
imgui.TreePop()
|
|
}
|
|
if imgui.TreeNodeV("Class Properties", imgui.TreeNodeFlagsFramed) {
|
|
view.renderProperties(lvl, objects, readOnly,
|
|
func(obj *level.ObjectMainEntry) *interpreters.Instance { return lvl.ObjectClassData(obj) })
|
|
view.renderBlockPuzzleControl(lvl, objects, readOnly)
|
|
imgui.TreePop()
|
|
}
|
|
|
|
imgui.PopItemWidth()
|
|
}
|
|
|
|
func (view *ObjectsView) renderProperties(lvl *level.Level, objects []*level.ObjectMainEntry, readOnly bool,
|
|
interpreterRetriever func(*level.ObjectMainEntry) *interpreters.Instance) {
|
|
propertyUnifier := make(map[string]*values.Unifier)
|
|
propertyDescribers := make(map[string]func(*interpreters.Simplifier))
|
|
var propertyOrder []string
|
|
describer := func(interpreter *interpreters.Instance, key string) func(simpl *interpreters.Simplifier) {
|
|
return func(simpl *interpreters.Simplifier) { interpreter.Describe(key, simpl) }
|
|
}
|
|
|
|
var unifyInterpreter func(string, *interpreters.Instance, bool, map[string]bool)
|
|
unifyInterpreter = func(path string, interpreter *interpreters.Instance, first bool, thisKeys map[string]bool) {
|
|
for _, key := range interpreter.Keys() {
|
|
fullPath := path + key
|
|
thisKeys[fullPath] = true
|
|
if unifier, existing := propertyUnifier[fullPath]; existing || first {
|
|
if !existing {
|
|
u := values.NewUnifier()
|
|
unifier = &u
|
|
propertyUnifier[fullPath] = unifier
|
|
propertyDescribers[fullPath] = describer(interpreter, key)
|
|
propertyOrder = append(propertyOrder, fullPath)
|
|
}
|
|
unifier.Add(int32(interpreter.Get(key)))
|
|
}
|
|
}
|
|
for _, key := range interpreter.ActiveRefinements() {
|
|
unifyInterpreter(path+key+".", interpreter.Refined(key), first, thisKeys)
|
|
}
|
|
}
|
|
|
|
for index, obj := range objects {
|
|
interpreter := interpreterRetriever(obj)
|
|
|
|
thisKeys := make(map[string]bool)
|
|
unifyInterpreter("", interpreter, index == 0, thisKeys)
|
|
{
|
|
var toRemove []string
|
|
for previousKey := range propertyUnifier {
|
|
if !thisKeys[previousKey] {
|
|
toRemove = append(toRemove, previousKey)
|
|
}
|
|
}
|
|
for _, key := range toRemove {
|
|
delete(propertyUnifier, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
lastTitle := ""
|
|
for _, key := range propertyOrder {
|
|
if unifier, existing := propertyUnifier[key]; existing {
|
|
subKeys := strings.Split(key, ".")
|
|
baseKey := ""
|
|
if len(subKeys) > 1 {
|
|
baseKey = subKeys[0]
|
|
}
|
|
if baseKey != lastTitle {
|
|
imgui.Separator()
|
|
imgui.Text(baseKey + ":")
|
|
lastTitle = baseKey
|
|
}
|
|
view.renderPropertyControl(lvl, readOnly, key, *unifier, propertyDescribers[key],
|
|
func(modifier func(uint32) uint32) {
|
|
view.requestPropertiesChange(interpreterRetriever, key, modifier) // nolint: scopelint
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (view *ObjectsView) renderPropertyControl(lvl *level.Level, readOnly bool,
|
|
fullKey string, unifier values.Unifier, describer func(*interpreters.Simplifier),
|
|
updater func(func(uint32) uint32)) {
|
|
keys := strings.Split(fullKey, ".")
|
|
key := keys[len(keys)-1]
|
|
label := key + "###" + fullKey
|
|
_, _, levelHeight := lvl.Size()
|
|
tileHeightFormatter := tileHeightFormatterFor(levelHeight)
|
|
objectHeightFormatter := objectHeightFormatterFor(levelHeight)
|
|
moveTileHeightFormatter := moveTileHeightFormatterFor(levelHeight)
|
|
|
|
objTypeRenderer := values.ObjectTypeControlRenderer{
|
|
Meta: view.gameObjects.AllProperties(),
|
|
TextCache: view.textCache,
|
|
}
|
|
simplifier := values.StandardSimplifier(readOnly, fullKey, unifier, updater, objTypeRenderer)
|
|
|
|
simplifier.SetObjectIDHandler(func() {
|
|
values.RenderUnifiedSliderInt(readOnly, label, unifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(int32)) },
|
|
func(value int) string { return "%d" },
|
|
0, lvl.ObjectCapacity(),
|
|
func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 { return uint32(newValue) })
|
|
})
|
|
})
|
|
|
|
addVariableKey := func() {
|
|
variableTypes := []string{"boolean", "Integer"}
|
|
typeMask := 0x1000
|
|
|
|
typeLabel := key + "-Type###" + fullKey + "-Type"
|
|
values.RenderUnifiedCombo(readOnly, typeLabel, unifier,
|
|
func(u values.Unifier) int {
|
|
value := u.Unified().(int32)
|
|
result := 0
|
|
if (value & int32(typeMask)) != 0 {
|
|
result = 1
|
|
}
|
|
return result
|
|
},
|
|
func(index int) string { return variableTypes[index] },
|
|
len(variableTypes),
|
|
func(newIndex int) {
|
|
setMask := uint32(0)
|
|
if newIndex > 0 {
|
|
setMask |= uint32(typeMask)
|
|
}
|
|
updater(func(oldValue uint32) uint32 {
|
|
return (oldValue & uint32(0xE000)) | setMask
|
|
})
|
|
})
|
|
|
|
valueLabel := key + "-Value###" + fullKey + "-Value"
|
|
switch {
|
|
case unifier.IsUnique():
|
|
key := unifier.Unified().(int32)
|
|
isIntegerVar := (key & int32(typeMask)) != 0
|
|
limit := archive.BooleanVarCount
|
|
if isIntegerVar {
|
|
limit = archive.IntegerVarCount
|
|
}
|
|
values.RenderUnifiedSliderInt(readOnly, valueLabel, unifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(int32)) & 0x1FF },
|
|
func(value int) string {
|
|
if isIntegerVar {
|
|
return fmt.Sprintf("%%d: %s", view.varInfoProvider.IntegerVariable(value).Name)
|
|
}
|
|
return fmt.Sprintf("%%d: %s", view.varInfoProvider.BooleanVariable(value).Name)
|
|
},
|
|
0, limit-1,
|
|
func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 {
|
|
result := oldValue & ^uint32(0x01FF)
|
|
result |= uint32(newValue) & uint32(0x01FF)
|
|
return result
|
|
})
|
|
})
|
|
|
|
case !unifier.IsUnique():
|
|
imgui.LabelText(valueLabel, "(multiple)")
|
|
default:
|
|
imgui.LabelText(valueLabel, "")
|
|
}
|
|
}
|
|
|
|
simplifier.SetSpecialHandler("VariableKey", addVariableKey)
|
|
simplifier.SetSpecialHandler("VariableCondition", func() {
|
|
addVariableKey()
|
|
|
|
comparisons := []string{
|
|
"Var == Val",
|
|
"Var < Val",
|
|
"Var <= Val",
|
|
"Var > Val",
|
|
"Var >= Val",
|
|
"Var != Val",
|
|
}
|
|
|
|
comboLabel := key + "-Check###" + fullKey + "-Value"
|
|
|
|
values.RenderUnifiedCombo(readOnly, comboLabel, unifier,
|
|
func(u values.Unifier) int {
|
|
key := unifier.Unified().(int32)
|
|
return int((key >> 13) & 0x7)
|
|
},
|
|
func(index int) string { return comparisons[index] },
|
|
len(comparisons),
|
|
func(newIndex int) {
|
|
updater(func(oldValue uint32) uint32 {
|
|
result := oldValue & ^uint32(0xE000)
|
|
result |= uint32(newIndex<<13) & uint32(0xE000)
|
|
return result
|
|
})
|
|
})
|
|
})
|
|
simplifier.SetSpecialHandler("LockVariable", func() {
|
|
limit := archive.BooleanVarCount
|
|
valueLabel := key + "###" + fullKey
|
|
values.RenderUnifiedSliderInt(readOnly, valueLabel, unifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(int32)) & 0x1FF },
|
|
func(value int) string {
|
|
return fmt.Sprintf("%%d: %s", view.varInfoProvider.BooleanVariable(value).Name)
|
|
},
|
|
0, limit-1,
|
|
func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 {
|
|
result := oldValue & ^uint32(0x01FF)
|
|
result |= uint32(newValue) & uint32(0x01FF)
|
|
return result
|
|
})
|
|
})
|
|
})
|
|
|
|
simplifier.SetSpecialHandler("BinaryCodedDecimal", func() {
|
|
values.RenderUnifiedSliderInt(readOnly, label, unifier,
|
|
func(u values.Unifier) int { return int(numbers.FromBinaryCodedDecimal(uint16(u.Unified().(int32)))) },
|
|
func(value int) string { return "%03d" },
|
|
0, 999,
|
|
func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 { return uint32(numbers.ToBinaryCodedDecimal(uint16(newValue))) })
|
|
})
|
|
})
|
|
|
|
simplifier.SetSpecialHandler("LevelTexture", func() {
|
|
atlas := lvl.TextureAtlas()
|
|
selectedIndex := -1
|
|
if unifier.IsUnique() {
|
|
selectedIndex = int(unifier.Unified().(int32))
|
|
}
|
|
|
|
values.RenderUnifiedSliderInt(readOnly, key+" (atlas index)###"+fullKey+"-atlas", unifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(int32)) },
|
|
func(value int) string { return "%d" },
|
|
0, len(atlas)-1,
|
|
func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 { return uint32(newValue) })
|
|
})
|
|
render.TextureSelector(label, -1, view.guiScale, len(atlas), selectedIndex,
|
|
view.textureCache,
|
|
func(index int) resource.Key {
|
|
return resource.KeyOf(ids.LargeTextures.Plus(int(atlas[index])), resource.LangAny, 0)
|
|
},
|
|
func(index int) string { return view.textureName(int(atlas[index])) },
|
|
func(index int) {
|
|
if !readOnly {
|
|
updater(func(oldValue uint32) uint32 { return uint32(index) })
|
|
}
|
|
})
|
|
})
|
|
simplifier.SetSpecialHandler("MaterialOrLevelTexture", func() {
|
|
types := []string{"Material", "Level Texture"}
|
|
selectedType := -1
|
|
selectedIndex := -1
|
|
if unifier.IsUnique() {
|
|
value := int(unifier.Unified().(int32))
|
|
selectedType = (value >> 7) & 1
|
|
selectedIndex = value & 0x7F
|
|
}
|
|
|
|
values.RenderUnifiedCombo(readOnly, key+"###"+fullKey+"-Type", unifier,
|
|
func(u values.Unifier) int { return selectedType },
|
|
func(index int) string { return types[index] },
|
|
len(types),
|
|
func(newIndex int) {
|
|
updater(func(oldValue uint32) uint32 {
|
|
newValue := uint32(0)
|
|
if newIndex > 0 {
|
|
newValue = 0x80
|
|
}
|
|
return newValue
|
|
})
|
|
})
|
|
selectorLabel := key + "-Texture###" + fullKey + "-Texture"
|
|
if selectedType == 0 {
|
|
resInfo, _ := ids.Info(ids.ObjectMaterialBitmaps)
|
|
render.TextureSelector(selectorLabel, -1, view.guiScale, resInfo.MaxCount, selectedIndex,
|
|
view.textureCache,
|
|
func(index int) resource.Key {
|
|
return resource.KeyOf(ids.ObjectMaterialBitmaps.Plus(index), resource.LangAny, 0)
|
|
},
|
|
func(index int) string { return fmt.Sprintf("%3d", index) },
|
|
func(index int) {
|
|
if !readOnly {
|
|
updater(func(oldValue uint32) uint32 { return uint32(index) })
|
|
}
|
|
})
|
|
} else {
|
|
atlas := lvl.TextureAtlas()
|
|
render.TextureSelector(selectorLabel, -1, view.guiScale, len(atlas), selectedIndex,
|
|
view.textureCache,
|
|
func(index int) resource.Key {
|
|
return resource.KeyOf(ids.LargeTextures.Plus(int(atlas[index])), resource.LangAny, 0)
|
|
},
|
|
func(index int) string { return view.textureName(int(atlas[index])) },
|
|
func(index int) {
|
|
if !readOnly {
|
|
updater(func(oldValue uint32) uint32 { return uint32(0x80 | index) })
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
simplifier.SetSpecialHandler("TileHeight", func() {
|
|
values.RenderUnifiedSliderInt(readOnly, label, unifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(int32)) },
|
|
tileHeightFormatter,
|
|
0, int(level.TileHeightUnitMax-1),
|
|
func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 { return uint32(newValue) })
|
|
})
|
|
})
|
|
simplifier.SetSpecialHandler("ObjectHeight", func() {
|
|
values.RenderUnifiedSliderInt(readOnly, label, unifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(int32)) },
|
|
objectHeightFormatter,
|
|
0, 255,
|
|
func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 { return uint32(newValue) })
|
|
})
|
|
})
|
|
simplifier.SetSpecialHandler("MoveTileHeight", func() {
|
|
values.RenderUnifiedSliderInt(readOnly, label, unifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(int32)) },
|
|
moveTileHeightFormatter,
|
|
0, 0x0FFF,
|
|
func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 { return uint32(newValue) })
|
|
})
|
|
})
|
|
|
|
simplifier.SetSpecialHandler("TileType", func() {
|
|
values.RenderUnifiedCombo(readOnly, label, unifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(int32)) },
|
|
func(index int) string { return level.TileType(index).String() },
|
|
len(level.TileTypes()),
|
|
func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 { return uint32(newValue) })
|
|
})
|
|
})
|
|
|
|
simplifier.SetSpecialHandler("MultiAnimation", func() {
|
|
/*
|
|
animationTypes := []string{"Random"}
|
|
selectedType := -1
|
|
if unifier.IsUnique() {
|
|
value := int(unifier.Unified().(int32))
|
|
selectedType = value >> 12
|
|
}
|
|
|
|
values.RenderUnifiedCombo(readOnly, key+"###"+fullKey+"-Type", unifier,
|
|
func(u values.Unifier) int {
|
|
key := unifier.Unified().(int32)
|
|
return int(key >> 12)
|
|
},
|
|
func(index int) string { return animationTypes[index] },
|
|
len(animationTypes),
|
|
func(newIndex int) {
|
|
updater(func(oldValue uint32) uint32 {
|
|
result := oldValue & ^uint32(0xF000)
|
|
result |= uint32(newIndex<<12) & uint32(0xF000)
|
|
return result
|
|
})
|
|
})
|
|
*/
|
|
|
|
// if selectedType == 0 {
|
|
values.RenderUnifiedSliderInt(readOnly, key+" infrequency###"+fullKey+"-infrequency", unifier,
|
|
func(u values.Unifier) int {
|
|
key := unifier.Unified().(int32)
|
|
return int(key & 0x0FFF)
|
|
},
|
|
func(value int) string { return "%d" },
|
|
0, 0x0FFF,
|
|
func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 {
|
|
result := oldValue & ^uint32(0x0FFF)
|
|
result |= uint32(newValue) & uint32(0x0FFF)
|
|
return result
|
|
})
|
|
})
|
|
//}
|
|
})
|
|
|
|
simplifier.SetSpecialHandler("PictureSource", func() {
|
|
indexConverter := func(u values.Unifier) int { return int(u.Unified().(int32)) & 0x007F }
|
|
indexUpdater := func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 {
|
|
result := oldValue & ^uint32(0x007F)
|
|
result |= uint32(newValue) & uint32(0x007F)
|
|
return result
|
|
})
|
|
}
|
|
|
|
sourceType := []string{"Data", "ObjectID"}
|
|
selectedType := -1
|
|
if unifier.IsUnique() {
|
|
value := int(unifier.Unified().(int32))
|
|
selectedType = (value & 0x1000) >> 12
|
|
}
|
|
|
|
imgui.Separator()
|
|
imgui.Text(key + ":")
|
|
|
|
values.RenderUnifiedCombo(readOnly, "Source###"+fullKey+"-Source", unifier,
|
|
func(u values.Unifier) int {
|
|
key := unifier.Unified().(int32)
|
|
return int((key & 0x1000) >> 12)
|
|
},
|
|
func(index int) string { return sourceType[index] },
|
|
len(sourceType),
|
|
func(newIndex int) {
|
|
updater(func(oldValue uint32) uint32 {
|
|
result := oldValue & ^uint32(0x1000)
|
|
result |= uint32(newIndex<<12) & uint32(0x1000)
|
|
return result
|
|
})
|
|
})
|
|
|
|
if selectedType == 0 {
|
|
dataType := []string{"Screen", "Custom", "Text", "Scrolling Text"}
|
|
textureData := -1
|
|
index := -1
|
|
selectedDataType := -1
|
|
|
|
if unifier.IsUnique() {
|
|
value := int(unifier.Unified().(int32))
|
|
textureData = value & 0x0FFF
|
|
index = textureData & 0x007F
|
|
selectedDataType = (value & 0x0180) >> 7
|
|
}
|
|
|
|
values.RenderUnifiedCombo(readOnly, "Type###"+fullKey+"-Type", unifier,
|
|
func(u values.Unifier) int {
|
|
key := unifier.Unified().(int32)
|
|
return int((key & 0x0180) >> 7)
|
|
},
|
|
func(index int) string { return dataType[index] },
|
|
len(dataType),
|
|
func(newIndex int) {
|
|
updater(func(oldValue uint32) uint32 {
|
|
result := oldValue & ^uint32(0x0180)
|
|
result |= uint32(newIndex<<7) & uint32(0x0180)
|
|
return result
|
|
})
|
|
})
|
|
|
|
if selectedDataType == 0 { // Screen
|
|
resInfo, _ := ids.Info(ids.ScreenTextures)
|
|
|
|
values.RenderUnifiedSliderInt(readOnly, "Index###"+fullKey+"-Index", unifier,
|
|
indexConverter,
|
|
func(value int) string { return "%d" },
|
|
0, resInfo.MaxCount-1,
|
|
indexUpdater)
|
|
|
|
render.TextureSelector(key+"-Screen###"+fullKey+"-Screen", -1, view.guiScale, resInfo.MaxCount, index,
|
|
view.textureCache,
|
|
func(index int) resource.Key {
|
|
return resource.KeyOf(ids.ScreenTextures.Plus(index), resource.LangAny, 0)
|
|
},
|
|
func(index int) string { return fmt.Sprintf("%3d", index) },
|
|
func(index int) {
|
|
if !readOnly {
|
|
indexUpdater(index)
|
|
}
|
|
})
|
|
} else if selectedDataType == 1 { // Custom
|
|
values.RenderUnifiedSliderInt(readOnly, "Index###"+fullKey+"-Index", unifier,
|
|
indexConverter,
|
|
func(value int) string {
|
|
if value >= 0x78 && value <= (0x78+8) {
|
|
return "%d - Camera"
|
|
} else if value == 0x77 {
|
|
return "%d - Static"
|
|
} else if value == 0x76 {
|
|
return "%d - SHODAN Proximity"
|
|
} else if value >= 0x70 && value <= (0x70+6) {
|
|
return "%d - Automap"
|
|
} else {
|
|
return "%d"
|
|
}
|
|
},
|
|
0, 0x007F,
|
|
indexUpdater)
|
|
|
|
resInfo, _ := ids.Info(ids.ObjectMaterialBitmaps)
|
|
if index < resInfo.MaxCount {
|
|
render.TextureImage("Preview", view.textureCache, resource.KeyOf(ids.ObjectMaterialBitmaps.Plus(index), resource.LangAny, 0), imgui.Vec2{X: 64 * view.guiScale, Y: 64 * view.guiScale})
|
|
}
|
|
} else if selectedDataType == 2 { // Text
|
|
values.RenderUnifiedSliderInt(readOnly, "Index###"+fullKey+"-Index", unifier,
|
|
indexConverter,
|
|
func(value int) string {
|
|
if value == 0x007F {
|
|
return "%d - (Random number)"
|
|
} else {
|
|
return "%d"
|
|
}
|
|
},
|
|
0, 0x007F,
|
|
indexUpdater)
|
|
} else if selectedDataType == 3 { // Scrolling Text
|
|
values.RenderUnifiedSliderInt(readOnly, "Index###"+fullKey+"-Index", unifier,
|
|
indexConverter,
|
|
func(value int) string { return "%d" },
|
|
0, 0x007F,
|
|
indexUpdater)
|
|
}
|
|
|
|
if selectedDataType == 2 || selectedDataType == 3 {
|
|
fontNames := []string{"Large Tech", "Small Tech"}
|
|
values.RenderUnifiedCombo(readOnly, "Font###"+fullKey+"-Font", unifier,
|
|
func(u values.Unifier) int {
|
|
key := unifier.Unified().(int32)
|
|
return int((key & 0x0800) >> 11)
|
|
},
|
|
func(value int) string { return fontNames[value] },
|
|
len(fontNames),
|
|
func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 {
|
|
result := oldValue & ^uint32(0x0800)
|
|
result |= uint32(newValue<<11) & uint32(0x0800)
|
|
return result
|
|
})
|
|
})
|
|
}
|
|
|
|
values.RenderUnifiedSliderInt(readOnly, "Scale###"+fullKey+"-Scale", unifier,
|
|
func(u values.Unifier) int {
|
|
key := unifier.Unified().(int32)
|
|
return int((key & 0x0600) >> 9)
|
|
},
|
|
func(value int) string { return "%d" },
|
|
0, 3,
|
|
func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 {
|
|
result := oldValue & ^uint32(0x0600)
|
|
result |= uint32(newValue<<9) & uint32(0x0600)
|
|
return result
|
|
})
|
|
})
|
|
} else {
|
|
// Indirection to another object
|
|
values.RenderUnifiedSliderInt(readOnly, "ObjectID###"+fullKey+"-Object", unifier,
|
|
func(u values.Unifier) int { return int(u.Unified().(int32)) & 0x0FFF },
|
|
func(value int) string { return "%d" },
|
|
0, lvl.ObjectCapacity(),
|
|
func(newValue int) {
|
|
updater(func(oldValue uint32) uint32 {
|
|
result := oldValue & ^uint32(0x0FFF)
|
|
result |= uint32(newValue) & uint32(0x0FFF)
|
|
return result
|
|
})
|
|
})
|
|
}
|
|
})
|
|
|
|
describer(simplifier)
|
|
}
|
|
|
|
func (view *ObjectsView) renderBlockPuzzleControl(lvl *level.Level, objects []*level.ObjectMainEntry, readOnly bool) {
|
|
var blockPuzzleData *interpreters.Instance
|
|
|
|
if len(objects) == 1 {
|
|
interpreter := lvl.ObjectClassData(objects[0])
|
|
isBlockPuzzle := interpreter.Refined("Puzzle").Get("Type") == 0x10
|
|
if isBlockPuzzle {
|
|
blockPuzzleData = interpreter.Refined("Puzzle").Refined("Block")
|
|
}
|
|
}
|
|
if blockPuzzleData != nil {
|
|
blockPuzzleDataID := level.ObjectID(blockPuzzleData.Get("StateStoreObjectID"))
|
|
dataObj := lvl.Object(blockPuzzleDataID)
|
|
blockPuzzleDataState := lvl.ObjectClassData(dataObj).Raw()
|
|
expectedTriple := object.TripleFrom(int(object.ClassTrap), 0, 1)
|
|
|
|
imgui.Separator()
|
|
if (dataObj != nil) && (dataObj.InUse != 0) &&
|
|
(dataObj.Triple() == expectedTriple) &&
|
|
(len(blockPuzzleDataState) >= (16 + 6)) {
|
|
blockLayout := blockPuzzleData.Get("Layout")
|
|
blockWidth := int((blockLayout >> 20) & 7)
|
|
blockHeight := int((blockLayout >> 24) & 7)
|
|
state := level.NewBlockPuzzleState(blockPuzzleDataState[6:6+16], blockHeight, blockWidth)
|
|
startRow := 1 + (7-blockHeight)/2
|
|
startColumn := 1 + (7-blockWidth)/2
|
|
var cells [9][9]string
|
|
|
|
placeConnector := func(side, offset int, text string) {
|
|
xOffsets := []int{offset, offset, -1, blockWidth}
|
|
yOffsets := []int{-1, blockHeight, offset, offset}
|
|
y := startRow + yOffsets[side]
|
|
x := startColumn + xOffsets[side]
|
|
|
|
if (x >= 0) && (x < 9) && (y >= 0) && (y < 9) {
|
|
cells[y][x] = text
|
|
}
|
|
}
|
|
stateMapping := []string{" ", "X", "+", "(+)", "F", "(F)", "H", "(H)"}
|
|
|
|
for row := 0; row < blockHeight; row++ {
|
|
for col := 0; col < blockWidth; col++ {
|
|
value := state.CellValue(row, col)
|
|
cells[startRow+row][startColumn+col] = stateMapping[value]
|
|
}
|
|
}
|
|
placeConnector(int((blockLayout>>7)&3), int((blockLayout>>4)&7), "S")
|
|
placeConnector(int((blockLayout>>15)&3), int((blockLayout>>12)&7), "D")
|
|
|
|
cellSize := imgui.Vec2{X: imgui.TextLineHeightWithSpacing() * 1.5, Y: imgui.TextLineHeightWithSpacing() * 1.5}
|
|
for x := 0; x < 9; x++ {
|
|
imgui.BeginGroup()
|
|
for y := 0; y < 9; y++ {
|
|
cellText := cells[y][x]
|
|
imgui.PushID(fmt.Sprintf("%d:%d", x, y))
|
|
if len(cellText) > 0 {
|
|
if imgui.ButtonV(cellText, cellSize) && !readOnly {
|
|
clickRow := y - startRow
|
|
clickCol := x - startColumn
|
|
if (clickRow >= 0) && (clickRow < blockHeight) && (clickCol >= 0) && (clickCol < blockWidth) {
|
|
oldValue := state.CellValue(clickRow, clickCol)
|
|
state.SetCellValue(clickRow, clickCol, (8+oldValue+1)%8)
|
|
|
|
// The following seems to do nothing, yet the data is still synchronized.
|
|
// This appears to be a bad design - maybe find a better way to design the concept
|
|
// of block puzzle references...
|
|
view.requestAction("ChangeBlockPuzzle", func() error {
|
|
return view.editor.ChangeObjects(func(entry *level.ObjectMainEntry) {})
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
imgui.Dummy(cellSize)
|
|
}
|
|
imgui.PopID()
|
|
}
|
|
imgui.EndGroup()
|
|
imgui.SameLine()
|
|
}
|
|
} else {
|
|
imgui.Text("No proper state store found for block puzzle!\n'StateStoreObjectID' must refer to a " + expectedTriple.String() + " object.")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (view *ObjectsView) requestChangeObjects(name string, modifier func(*level.ObjectMainEntry)) {
|
|
view.requestAction(name, func() error {
|
|
return view.editor.ChangeObjects(modifier)
|
|
})
|
|
}
|
|
|
|
func (view *ObjectsView) requestPropertiesChange(interpreterRetriever func(*level.ObjectMainEntry) *interpreters.Instance,
|
|
key string, modifier func(uint32) uint32) {
|
|
subKeys := strings.Split(key, ".")
|
|
valueIndex := len(subKeys) - 1
|
|
|
|
view.requestAction("ChangeProperties", func() error {
|
|
return view.editor.ChangeObjects(func(obj *level.ObjectMainEntry) {
|
|
interpreter := interpreterRetriever(obj)
|
|
for subIndex := 0; subIndex < valueIndex; subIndex++ {
|
|
interpreter = interpreter.Refined(subKeys[subIndex])
|
|
}
|
|
subKey := subKeys[valueIndex]
|
|
interpreter.Set(subKey, modifier(interpreter.Get(subKey)))
|
|
})
|
|
})
|
|
}
|
|
|
|
func (view *ObjectsView) requestDeleteObjects() {
|
|
view.requestAction("DeleteObjects", view.editor.DeleteObjects)
|
|
}
|
|
|
|
func (view *ObjectsView) requestAction(name string, nested func() error) {
|
|
err := view.registry.Register(cmd.Named(name),
|
|
cmd.Forward(view.restoreFocusTask()),
|
|
cmd.Nested(nested),
|
|
cmd.Reverse(view.restoreFocusTask()))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (view *ObjectsView) restoreFocusTask() cmd.Task {
|
|
return func(modder world.Modder) error {
|
|
view.model.restoreFocus = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (view *ObjectsView) tripleName(triple object.Triple) string {
|
|
suffix := hintUnknown
|
|
linearIndex := view.gameObjects.AllProperties().TripleIndex(triple)
|
|
if linearIndex >= 0 {
|
|
key := resource.KeyOf(ids.ObjectLongNames, resource.LangDefault, linearIndex)
|
|
objName, err := view.textCache.Text(key)
|
|
if err == nil {
|
|
suffix = objName
|
|
}
|
|
}
|
|
return triple.String() + ": " + suffix
|
|
}
|
|
|
|
func (view *ObjectsView) 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
|
|
}
|