Files
questshock/modding/hacked/editor/archives/View.go
T

935 lines
29 KiB
Go

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", "")
}