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 }