Adding modding folder with a hacked build.

This commit is contained in:
ml
2026-09-26 08:53:08 +02:00
parent cdb960dc71
commit c91080aba7
755 changed files with 50424 additions and 0 deletions
@@ -0,0 +1,125 @@
package archives
import (
"github.com/inkyblackness/imgui-go/v3"
"github.com/inkyblackness/hacked/editor/values"
"github.com/inkyblackness/hacked/ss1/content/archive"
"github.com/inkyblackness/hacked/ss1/edit"
"github.com/inkyblackness/hacked/ui/gui"
)
type editBooleanVariableDialog struct {
machine gui.ModalStateMachine
view *View
service *edit.GameStateService
index int
varOverride bool
info archive.GameVariableInfo
opened bool
}
func (dialog *editBooleanVariableDialog) Render() {
if !dialog.opened {
dialog.opened = true
dialog.varOverride = dialog.service.BooleanVariableOverride(dialog.index)
dialog.info = dialog.service.BooleanVariable(dialog.index)
imgui.OpenPopup("Edit boolean variable")
}
imgui.SetNextWindowSize(imgui.Vec2{X: 600, Y: 0}.Times(dialog.view.guiScale))
if imgui.BeginPopupModalV("Edit boolean variable", nil,
imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
dialog.renderControls()
imgui.Separator()
if imgui.Button("OK") {
dialog.applyChanges()
dialog.close()
}
imgui.SameLine()
if imgui.Button("Cancel") {
dialog.close()
}
imgui.EndPopup()
} else {
dialog.machine.SetState(nil)
}
}
func (dialog *editBooleanVariableDialog) applyChanges() {
if dialog.varOverride {
_ = dialog.service.SetBooleanVariable(dialog.index, dialog.info)
} else {
_ = dialog.service.DefaultBooleanVariable(dialog.index)
}
}
func (dialog *editBooleanVariableDialog) close() {
dialog.machine.SetState(nil)
imgui.CloseCurrentPopup()
}
func (dialog *editBooleanVariableDialog) renderControls() {
imgui.PushItemWidth(-150 * dialog.view.guiScale)
values.RenderUnifiedCheckboxCombo(false, "Override", values.UnifierFor(dialog.varOverride), func(newValue bool) {
dialog.varOverride = newValue
})
textFlags := imgui.InputTextFlagsNoUndoRedo
if !dialog.varOverride {
textFlags |= imgui.InputTextFlagsReadOnly
}
imgui.InputTextV("Name", &dialog.info.Name, textFlags, nil)
imgui.InputTextMultilineV("Description", &dialog.info.Description, imgui.Vec2{X: 0, Y: 100 * dialog.view.guiScale},
textFlags, nil)
imgui.Separator()
defaultValues := map[int]string{0: "False", 1: "True"}
valueLabels := map[bool]string{false: "Default", true: "Special"}
hasSpecialValues := dialog.info.ValueNames != nil
if dialog.varOverride {
if imgui.BeginCombo("Values", valueLabels[hasSpecialValues]) {
if imgui.SelectableV(valueLabels[false], !hasSpecialValues, 0, imgui.Vec2{}) {
dialog.info.ValueNames = nil
}
if imgui.SelectableV(valueLabels[true], hasSpecialValues, 0, imgui.Vec2{}) {
dialog.info.ValueNames = map[int16]string{0: defaultValues[0], 1: defaultValues[1]}
}
imgui.EndCombo()
}
} else {
imgui.LabelText("Values", valueLabels[hasSpecialValues])
}
hasSpecialValues = dialog.info.ValueNames != nil
if hasSpecialValues {
for i := int16(0); i < 2; i++ {
text := dialog.info.ValueNames[i]
if imgui.InputTextV(defaultValues[int(i)], &text, textFlags, nil) {
dialog.info.ValueNames[i] = text
}
}
} else {
imgui.LabelText(defaultValues[0], defaultValues[0])
imgui.LabelText(defaultValues[1], defaultValues[1])
}
initUnifier := values.UnifierFor(dialog.info.ResetValueInt())
values.RenderUnifiedCombo(dialog.info.Hardcoded || !dialog.varOverride, "Reset Value", initUnifier,
func(u values.Unifier) int { return int(u.Unified().(int16)) },
func(value int) string {
name, found := dialog.info.ValueNames[int16(value)]
if found {
return name
}
return defaultValues[value]
},
2,
func(newValue int) {
value16 := int16(newValue)
dialog.info.InitValue = &value16
})
imgui.PopItemWidth()
}
@@ -0,0 +1,214 @@
package archives
import (
"fmt"
"math"
"sort"
"github.com/inkyblackness/imgui-go/v3"
"github.com/inkyblackness/hacked/editor/values"
"github.com/inkyblackness/hacked/ss1/content/archive"
"github.com/inkyblackness/hacked/ss1/edit"
"github.com/inkyblackness/hacked/ui/gui"
)
type editIntegerVariableDialog struct {
machine gui.ModalStateMachine
view *View
service *edit.GameStateService
index int
varOverride bool
info archive.GameVariableInfo
selectedEnum int
opened bool
}
func (dialog *editIntegerVariableDialog) Render() {
if !dialog.opened {
dialog.opened = true
dialog.varOverride = dialog.service.IntegerVariableOverride(dialog.index)
dialog.info = dialog.service.IntegerVariable(dialog.index)
imgui.OpenPopup("Edit integer variable")
}
imgui.SetNextWindowSize(imgui.Vec2{X: 600, Y: 0}.Times(dialog.view.guiScale))
if imgui.BeginPopupModalV("Edit integer variable", nil,
imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsAlwaysAutoResize) {
dialog.renderControls()
imgui.Separator()
if imgui.Button("OK") {
dialog.applyChanges()
dialog.close()
}
imgui.SameLine()
if imgui.Button("Cancel") {
dialog.close()
}
imgui.EndPopup()
} else {
dialog.machine.SetState(nil)
}
}
func (dialog *editIntegerVariableDialog) applyChanges() {
if dialog.varOverride {
_ = dialog.service.SetIntegerVariable(dialog.index, dialog.info)
} else {
_ = dialog.service.DefaultIntegerVariable(dialog.index)
}
}
func (dialog *editIntegerVariableDialog) close() {
dialog.machine.SetState(nil)
imgui.CloseCurrentPopup()
}
func (dialog *editIntegerVariableDialog) renderControls() {
imgui.PushItemWidth(-150 * dialog.view.guiScale)
values.RenderUnifiedCheckboxCombo(false, "Override", values.UnifierFor(dialog.varOverride), func(newValue bool) {
dialog.varOverride = newValue
})
textFlags := imgui.InputTextFlagsNoUndoRedo
if !dialog.varOverride {
textFlags |= imgui.InputTextFlagsReadOnly
}
imgui.InputTextV("Name", &dialog.info.Name, textFlags, nil)
imgui.InputTextMultilineV("Description", &dialog.info.Description, imgui.Vec2{X: 0, Y: 100 * dialog.view.guiScale},
textFlags, nil)
imgui.Separator()
valueLabels := map[bool]string{false: "Numeric", true: "Enumerated"}
isEnumerated := len(dialog.info.ValueNames) > 0
if imgui.BeginCombo("Values", valueLabels[isEnumerated]) {
if imgui.SelectableV(valueLabels[false], !isEnumerated, 0, imgui.Vec2{}) {
dialog.info.ValueNames = nil
}
if imgui.SelectableV(valueLabels[true], isEnumerated, 0, imgui.Vec2{}) {
dialog.info.ValueNames = map[int16]string{0: "Zero"}
dialog.selectedEnum = 0
}
imgui.EndCombo()
}
if len(dialog.info.ValueNames) > 0 {
gui.StepSliderInt("Index", &dialog.selectedEnum, math.MinInt16, math.MaxInt16)
if dialog.varOverride {
text := dialog.info.ValueNames[int16(dialog.selectedEnum)]
if imgui.InputTextV("Value", &text, textFlags, nil) {
if len(text) > 0 {
dialog.info.ValueNames[int16(dialog.selectedEnum)] = text
} else {
delete(dialog.info.ValueNames, int16(dialog.selectedEnum))
if len(dialog.info.ValueNames) == 0 {
dialog.info.ValueNames[0] = "Zero"
}
}
}
} else {
imgui.LabelText("Value", dialog.info.ValueNames[int16(dialog.selectedEnum)])
}
resetValue := dialog.info.ResetValueInt()
enumEntries := toEnumEntries(dialog.info.ValueNames)
linearInitIndex := linearIndex(enumEntries, resetValue)
initUnifier := values.UnifierFor(linearInitIndex)
values.RenderUnifiedCombo(dialog.info.Hardcoded || !dialog.varOverride, "Reset Value", initUnifier,
func(u values.Unifier) int { return u.Unified().(int) },
func(value int) string {
if value < 0 {
return fmt.Sprintf("%d: (unmapped)", resetValue)
}
entry := enumEntries[value]
return fmt.Sprintf("%d: '%s'", entry.index, entry.value)
},
len(enumEntries),
func(newValue int) {
dialog.info.InitValue = &enumEntries[newValue].index
})
} else {
minValue := math.MinInt16
maxValue := math.MaxInt16
if dialog.info.Limits != nil {
minValue = int(dialog.info.Limits.Minimum)
maxValue = int(dialog.info.Limits.Maximum)
}
if minValue > maxValue {
maxValue = minValue
}
if dialog.varOverride {
if gui.StepSliderInt("Minimum", &minValue, math.MinInt16, math.MaxInt16) {
if minValue > maxValue {
maxValue = minValue
}
}
if gui.StepSliderInt("Maximum", &maxValue, math.MinInt16, math.MaxInt16) {
if maxValue < minValue {
minValue = maxValue
}
}
if (minValue == math.MinInt16) && (maxValue == math.MaxInt16) {
dialog.info.Limits = nil
} else {
if dialog.info.Limits == nil {
dialog.info.Limits = &archive.GameVariableLimits{}
}
dialog.info.Limits.Minimum = int16(minValue)
dialog.info.Limits.Maximum = int16(maxValue)
}
} else {
imgui.LabelText("Minimum", fmt.Sprintf("%d", minValue))
imgui.LabelText("Maximum", fmt.Sprintf("%d", maxValue))
}
resetValue := int(dialog.info.ResetValueInt())
if dialog.info.Hardcoded || !dialog.varOverride {
imgui.LabelText("Reset Value", fmt.Sprintf("%d", resetValue))
} else if gui.StepSliderInt("Reset Value", &resetValue, minValue, maxValue) {
resetValue16 := int16(resetValue)
dialog.info.InitValue = &resetValue16
}
}
imgui.PopItemWidth()
}
type enumEntry struct {
index int16
value string
}
func toEnumEntries(mapped map[int16]string) []enumEntry {
minIndex := math.MaxInt16
maxIndex := math.MinInt16
for index := range mapped {
if int(index) < minIndex {
minIndex = int(index)
}
if int(index) > maxIndex {
maxIndex = int(index)
}
}
orderedEntries := make([]enumEntry, 0, len(mapped))
for index, value := range mapped {
orderedEntries = append(orderedEntries, enumEntry{
index: index,
value: value,
})
}
sort.Slice(orderedEntries, func(a, b int) bool { return orderedEntries[a].index < orderedEntries[b].index })
return orderedEntries
}
func linearIndex(entries []enumEntry, index int16) int {
for linear, entry := range entries {
if entry.index == index {
return linear
}
}
return -1
}
@@ -0,0 +1,45 @@
package archives
import (
"github.com/inkyblackness/hacked/ss1/resource"
"github.com/inkyblackness/hacked/ss1/world"
)
type setArchiveDataCommand struct {
model *viewModel
selectedLevel int
oldData map[resource.ID][]byte
newData map[resource.ID][]byte
}
func (command setArchiveDataCommand) Do(modder world.Modder) error {
command.delResources(modder, command.oldData)
return command.perform(modder, command.newData)
}
func (command setArchiveDataCommand) Undo(modder world.Modder) error {
command.delResources(modder, command.newData)
return command.perform(modder, command.oldData)
}
func (command setArchiveDataCommand) delResources(modder world.Modder, data map[resource.ID][]byte) {
for id := range data {
modder.DelResource(resource.LangAny, id)
}
}
// nolint: interfacer
func (command setArchiveDataCommand) perform(modder world.Modder, data map[resource.ID][]byte) error {
for id, blockData := range data {
if len(blockData) > 0 {
modder.SetResourceBlocks(resource.LangAny, id, [][]byte{blockData})
} else {
modder.DelResource(resource.LangAny, id)
}
}
command.model.restoreFocus = true
command.model.selectedLevel = command.selectedLevel
return nil
}
+934
View File
@@ -0,0 +1,934 @@
package archives
import (
"fmt"
"math"
"strings"
"github.com/inkyblackness/imgui-go/v3"
"github.com/inkyblackness/hacked/editor/values"
"github.com/inkyblackness/hacked/ss1/content/archive"
"github.com/inkyblackness/hacked/ss1/content/archive/level"
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlids"
"github.com/inkyblackness/hacked/ss1/content/archive/level/lvlobj"
"github.com/inkyblackness/hacked/ss1/content/interpreters"
"github.com/inkyblackness/hacked/ss1/content/numbers"
"github.com/inkyblackness/hacked/ss1/content/object"
"github.com/inkyblackness/hacked/ss1/content/text"
"github.com/inkyblackness/hacked/ss1/edit"
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
"github.com/inkyblackness/hacked/ss1/resource"
"github.com/inkyblackness/hacked/ss1/world"
"github.com/inkyblackness/hacked/ss1/world/citadel"
"github.com/inkyblackness/hacked/ss1/world/ids"
"github.com/inkyblackness/hacked/ui/gui"
)
const (
hintUnknown = "???"
)
var variableContextList = map[edit.VariableBaseContextIdentifier]struct {
title string
info string
}{
edit.VariableContextCitadel: {
title: "Citadel Mission",
info: "Variables are listed based on use on Citadel.\n" +
"Useful for Citadel-based savegames and mods, such as 'New Game+' variants.",
},
edit.VariableContextEngine: {
title: "Engine",
info: "Variables are listed based on the hardcoded engine behaviour.\n" +
"Useful for dedicated missions.",
},
}
// View provides edit controls for the archive.
type View struct {
registry cmd.Registry
gameStateService *edit.GameStateService
mod *world.Mod
textCache *text.Cache
cp text.Codepage
modalStateMachine gui.ModalStateMachine
guiScale float32
commander cmd.Commander
model viewModel
}
// NewArchiveView returns a new instance.
func NewArchiveView(registry cmd.Registry,
gameStateService *edit.GameStateService, mod *world.Mod,
textCache *text.Cache, cp text.Codepage,
modalStateMachine gui.ModalStateMachine, guiScale float32, commander cmd.Commander) *View {
view := &View{
registry: registry,
gameStateService: gameStateService,
mod: mod,
textCache: textCache,
cp: cp,
modalStateMachine: modalStateMachine,
guiScale: guiScale,
commander: commander,
model: freshViewModel(),
}
return view
}
// WindowOpen returns the flag address, to be used with the main menu.
func (view *View) WindowOpen() *bool {
return &view.model.windowOpen
}
// Render renders the view.
func (view *View) Render() {
if view.model.restoreFocus {
imgui.SetNextWindowFocus()
view.model.restoreFocus = false
view.model.windowOpen = true
}
if view.model.windowOpen {
imgui.SetNextWindowSizeV(imgui.Vec2{X: 650 * view.guiScale, Y: 400 * view.guiScale}, imgui.ConditionFirstUseEver)
if imgui.BeginV("Archive", view.WindowOpen(), imgui.WindowFlagsNoCollapse) {
view.renderContent()
}
imgui.End()
}
}
func (view *View) renderContent() {
imgui.BeginTabBar("archive-tab")
if imgui.BeginTabItem("Levels") {
view.renderLevelsContent()
imgui.EndTabItem()
}
if imgui.BeginTabItem("Game State") {
view.renderGameStateContent()
imgui.EndTabItem()
}
imgui.EndTabBar()
}
func (view *View) renderLevelsContent() {
imgui.BeginChildV("Levels", imgui.Vec2{X: -100 * view.guiScale, Y: 0}, true, 0)
for id := 0; id < archive.MaxLevels; id++ {
inMod := view.hasLevelInMod(id)
info := fmt.Sprintf("%d", id)
if !inMod {
if view.hasGameStateInMod() && (id == view.effectiveGameState().CurrentLevel()) {
// current level should really be in the archive.
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1.0, Y: 0.0, Z: 0.0, W: 1.0})
} else {
imgui.PushStyleColor(imgui.StyleColorText, imgui.Vec4{X: 1.0, Y: 1.0, Z: 1.0, W: 0.8})
}
info += " (not in mod, read-only)"
}
if imgui.SelectableV(info, id == view.model.selectedLevel, 0, imgui.Vec2{}) {
view.model.selectedLevel = id
}
if !inMod {
imgui.PopStyleColor()
}
}
imgui.EndChild()
imgui.SameLine()
imgui.BeginGroup()
if imgui.ButtonV("Clear", imgui.Vec2{X: -1, Y: 0}) {
view.requestClearLevel(view.model.selectedLevel)
}
if imgui.ButtonV("Remove", imgui.Vec2{X: -1, Y: 0}) {
view.requestRemoveLevel(view.model.selectedLevel)
}
imgui.EndGroup()
}
func (view *View) renderGameStateContent() {
data := view.gameStateData()
modStateIsDefaulting := false
imgui.PushItemWidth(-350 * view.guiScale)
if data.isPresent() {
modState := data.toInstance()
if !modState.IsSavegame() {
modStateIsDefaulting = modState.IsDefaulting()
if modStateIsDefaulting {
if imgui.Button("Override") {
view.requestSetGameState(citadel.DefaultGameState().Raw())
}
if imgui.IsItemHovered() {
imgui.SetTooltip("Values of new game archives are only considered by special engines.\n" +
"Overriding them will only work in those.")
}
} else if imgui.Button("Remove") {
view.requestSetGameState(archive.ZeroGameStateData())
}
}
}
readOnly := false
var editState *archive.GameState
if !data.isPresent() || modStateIsDefaulting {
editState = citadel.DefaultGameState()
readOnly = true
} else {
editState = data.imported().toInstance()
}
onChange := func() {
view.requestSetGameState(editState.Raw())
}
if imgui.TreeNodeV("Game State: General", imgui.TreeNodeFlagsDefaultOpen|imgui.TreeNodeFlagsFramed) {
imgui.LabelText("Hacker Name", "\""+editState.HackerName(view.cp)+"\"")
view.createPropertyControls(readOnly, editState.Instance, func(key string, modifier func(uint32) uint32) {
view.setInterpreterValueKeyed(editState.Instance, key, modifier)
onChange()
})
imgui.TreePop()
}
if imgui.TreeNodeV("Inventory", imgui.TreeNodeFlagsFramed) {
view.createInventoryControls(readOnly, editState, onChange)
imgui.TreePop()
}
if imgui.TreeNodeV("Hard-/Software", imgui.TreeNodeFlagsFramed) {
view.createWareControls(readOnly, editState, onChange)
imgui.TreePop()
}
if imgui.TreeNodeV("Game State: Variables", imgui.TreeNodeFlagsFramed) {
view.createVariableControls(readOnly, editState, onChange)
imgui.TreePop()
}
if imgui.TreeNodeV("Game State: Messages", imgui.TreeNodeFlagsFramed) {
view.createMessageControls(readOnly, editState, onChange)
imgui.TreePop()
}
imgui.PopItemWidth()
}
type gameStateData struct{ raw []byte }
func (data gameStateData) isPresent() bool {
return len(data.raw) > 0
}
func (data gameStateData) toInstance() *archive.GameState {
return archive.NewGameState(data.raw)
}
func (data gameStateData) snapshot() gameStateData {
copied := make([]byte, len(data.raw))
copy(copied, data.raw)
return gameStateData{raw: copied}
}
func (data gameStateData) imported() gameStateData {
// Avoid truncating data when working with savegames.
if len(data.raw) >= archive.GameStateSize {
return data.snapshot()
}
copied := archive.ZeroGameStateData()
copy(copied, data.raw)
return gameStateData{raw: copied}
}
func (view *View) gameStateData() gameStateData {
raw := view.mod.ModifiedBlock(resource.LangAny, ids.GameState, 0)
return gameStateData{raw: raw}
}
func (view *View) effectiveGameState() *archive.GameState {
state := view.gameStateData().toInstance()
if state.IsDefaulting() {
return citadel.DefaultGameState()
}
return state
}
func (view *View) hasGameStateInMod() bool {
return len(view.mod.ModifiedBlocks(resource.LangAny, ids.GameState)) > 0
}
func (view *View) hasLevelInMod(id int) bool {
return len(view.mod.ModifiedBlocks(resource.LangAny, ids.LevelResourcesStart.Plus(lvlids.PerLevel*id+lvlids.FirstUsed))) > 0
}
func (view *View) requestClearLevel(id int) {
if (id >= 0) && (id < archive.MaxLevels) {
command := setArchiveDataCommand{
model: &view.model,
selectedLevel: id,
newData: make(map[resource.ID][]byte),
oldData: make(map[resource.ID][]byte),
}
if !view.hasGameStateInMod() {
command.newData[ids.ArchiveName] = text.DefaultCodepage().Encode("Starting Game | by InkyBlackness HackEd")
command.newData[ids.GameState] = archive.ZeroGameStateData()
}
param := level.EmptyLevelParameters{
Cyberspace: world.IsConsideredCyberspaceByDefault(id),
}
effectiveGameState := view.effectiveGameState()
if id == effectiveGameState.CurrentLevel() {
posX, posY := effectiveGameState.HackerMapPosition()
param.MapModifier = func(x, y int, entry *level.TileMapEntry) {
if (x == int(posX.Tile())) && (y == int(posY.Tile())) {
entry.Type = level.TileTypeOpen
}
}
}
levelData := level.EmptyLevelData(param)
levelIDBegin := ids.LevelResourcesStart.Plus(lvlids.PerLevel * id)
for offset, newData := range &levelData {
if (offset < lvlids.FirstUsed) || (offset >= lvlids.FirstUnused) {
continue
}
resourceID := levelIDBegin.Plus(offset)
oldData := view.mod.ModifiedBlock(resource.LangAny, resourceID, 0)
if len(oldData) > 0 {
command.oldData[resourceID] = oldData
}
if len(newData) > 0 {
command.newData[resourceID] = newData
}
}
view.commander.Queue(command)
}
}
func (view *View) requestRemoveLevel(id int) {
if (id >= 0) && (id < archive.MaxLevels) && view.hasLevelInMod(id) {
command := setArchiveDataCommand{
model: &view.model,
selectedLevel: id,
newData: make(map[resource.ID][]byte),
oldData: make(map[resource.ID][]byte),
}
levelIDBegin := ids.LevelResourcesStart.Plus(lvlids.PerLevel * id)
for offset := lvlids.FirstUsed; offset < lvlids.PerLevel; offset++ {
resourceID := levelIDBegin.Plus(offset)
oldData := view.mod.ModifiedBlock(resource.LangAny, resourceID, 0)
if len(oldData) > 0 {
command.oldData[resourceID] = oldData
}
}
view.commander.Queue(command)
}
}
func (view *View) requestSetGameState(newData []byte) {
command := setArchiveDataCommand{
model: &view.model,
selectedLevel: view.model.selectedLevel,
newData: make(map[resource.ID][]byte),
oldData: make(map[resource.ID][]byte),
}
command.oldData[ids.GameState] = view.gameStateData().snapshot().raw
command.newData[ids.GameState] = newData
view.commander.Queue(command)
}
func (view *View) createPropertyControls(readOnly bool, rootInterpreter *interpreters.Instance,
updater func(string, func(uint32) uint32)) {
objTypeRenderer := values.ObjectTypeControlRenderer{
Meta: view.mod.ObjectProperties(),
TextCache: view.textCache,
}
var processInterpreter func(string, *interpreters.Instance)
processInterpreter = func(path string, interpreter *interpreters.Instance) {
for _, key := range interpreter.Keys() {
fullKey := path + key
unifier := values.NewUnifier()
unifier.Add(int32(interpreter.Get(key)))
simplifier := values.StandardSimplifier(readOnly, fullKey, unifier,
func(modifier func(uint32) uint32) {
updater(fullKey, modifier)
}, objTypeRenderer)
interpreter.Describe(key, simplifier)
}
for _, key := range interpreter.ActiveRefinements() {
fullKey := path + key
if len(fullKey) > 0 {
imgui.Separator()
imgui.Text(fullKey + ":")
}
processInterpreter(fullKey+".", interpreter.Refined(key))
}
}
processInterpreter("", rootInterpreter)
}
func (view *View) setInterpreterValueKeyed(instance *interpreters.Instance, key string, modifier func(uint32) uint32) {
resolvedInterpreter := instance
keys := strings.Split(key, ".")
keyCount := len(keys)
if len(keys) > 1 {
for _, subKey := range keys[:keyCount-1] {
resolvedInterpreter = resolvedInterpreter.Refined(subKey)
}
}
resolvedInterpreter.Set(keys[keyCount-1], modifier(resolvedInterpreter.Get(keys[keyCount-1])))
}
func (view *View) createInventoryControls(readOnly bool, gameState *archive.GameState, onChange func()) {
if imgui.TreeNodeV("Weapons", imgui.TreeNodeFlagsFramed) {
for i := 0; i < archive.InventoryWeaponSlots; i++ {
if i > 0 {
imgui.Separator()
}
imgui.PushID(fmt.Sprintf("weapon%d", i))
view.createInventoryWeaponSlotControls(readOnly, gameState.InventoryWeaponSlot(i), onChange)
imgui.PopID()
}
imgui.TreePop()
}
if imgui.TreeNodeV("Ammo", imgui.TreeNodeFlagsFramed) {
for i := 0; i < archive.AmmoTypeCount; i++ {
imgui.Separator()
imgui.PushID(fmt.Sprintf("ammo%d", i))
view.createInventoryAmmoControls(readOnly, gameState.InventoryAmmo(i), onChange)
imgui.PopID()
}
imgui.TreePop()
}
if imgui.TreeNodeV("Grenades", imgui.TreeNodeFlagsFramed) {
for i := 0; i < archive.GrenadeTypeCount; i++ {
imgui.Separator()
imgui.PushID(fmt.Sprintf("grenade%d", i))
view.createInventoryGrenadeControls(readOnly, gameState.InventoryGrenade(i), onChange)
imgui.PopID()
}
imgui.TreePop()
}
if imgui.TreeNodeV("Patches", imgui.TreeNodeFlagsFramed) {
for i := 0; i < archive.GrenadeTypeCount; i++ {
imgui.PushID(fmt.Sprintf("patch%d", i))
view.createPatchStateControls(readOnly, gameState.PatchState(i), onChange)
imgui.PopID()
}
imgui.TreePop()
}
if imgui.TreeNodeV("General", imgui.TreeNodeFlagsFramed) {
for i := 0; i < archive.GeneralInventorySlotCount; i++ {
imgui.PushID(fmt.Sprintf("general%d", i))
view.createGeneralInventoryControls(readOnly, gameState.GeneralInventorySlot(i), onChange)
imgui.PopID()
}
imgui.TreePop()
}
}
func (view *View) createWareControls(readOnly bool, gameState *archive.GameState, onChange func()) {
if imgui.TreeNodeV("Hardware", imgui.TreeNodeFlagsFramed) {
for i := 0; i < archive.HardwareTypeCount; i++ {
imgui.PushID(fmt.Sprintf("hw%d", i))
view.createHardwareControls(readOnly, gameState.HardwareState(i), onChange)
imgui.PopID()
}
imgui.TreePop()
}
if imgui.TreeNodeV("Software", imgui.TreeNodeFlagsFramed) {
for i := 0; i < archive.SoftwareOffenseTypeCount; i++ {
imgui.PushID(fmt.Sprintf("swoffense%d", i))
view.createVersionedSoftwareControls(readOnly, gameState.VersionedOffenseSoftwareState(i), 0, onChange)
imgui.PopID()
}
for i := 0; i < archive.SoftwareDefenseTypeCount; i++ {
imgui.PushID(fmt.Sprintf("swdefense%d", i))
view.createVersionedSoftwareControls(readOnly, gameState.VersionedDefenseSoftwareState(i), 1, onChange)
imgui.PopID()
}
for i := 0; i < archive.SoftwareOneshotTypeCount; i++ {
imgui.PushID(fmt.Sprintf("swoneshot%d", i))
view.createCountedSoftwareControls(readOnly, gameState.OneshotSoftwareState(i), 2, onChange)
imgui.PopID()
}
imgui.TreePop()
}
}
func (view *View) createHardwareControls(readOnly bool, state archive.HardwareState, onChange func()) {
values.RenderUnifiedSliderInt(readOnly,
fmt.Sprintf("Version (%s)", view.indexedName(object.ClassHardware, state.Index)),
values.UnifierFor(state.Version()),
func(u values.Unifier) int {
return u.Unified().(int)
}, func(value int) string {
return "%d"
}, 0, 4,
func(newValue int) {
state.SetVersion(newValue)
onChange()
})
values.RenderUnifiedCheckboxCombo(readOnly, "Is Active", values.UnifierFor(state.IsActive()), func(newActive bool) {
state.SetActive(newActive)
onChange()
})
}
func (view *View) createVersionedSoftwareControls(readOnly bool, sw archive.VersionedSoftwareState, subclass int, onChange func()) {
values.RenderUnifiedSliderInt(readOnly,
fmt.Sprintf("Version (%s)", view.tripleName(object.TripleFrom(int(object.ClassSoftware), subclass, sw.Index))),
values.UnifierFor(sw.Version()),
func(u values.Unifier) int {
return u.Unified().(int)
}, func(value int) string {
return "%d"
}, 0, 0xFF,
func(newValue int) {
sw.SetVersion(newValue)
onChange()
})
}
func (view *View) createCountedSoftwareControls(readOnly bool, sw archive.CountedSoftwareState, subclass int, onChange func()) {
values.RenderUnifiedSliderInt(readOnly,
fmt.Sprintf("Count (%s)", view.tripleName(object.TripleFrom(int(object.ClassSoftware), subclass, sw.Index))),
values.UnifierFor(sw.Count()),
func(u values.Unifier) int {
return u.Unified().(int)
}, func(value int) string {
return "%d"
}, 0, 0xFF,
func(newValue int) {
sw.SetCount(newValue)
onChange()
})
}
func (view *View) createGeneralInventoryControls(readOnly bool, slot archive.GeneralInventorySlot, onChange func()) {
values.RenderUnifiedSliderInt(readOnly,
fmt.Sprintf("ObjectIndex (Slot %d)", slot.Index+1),
values.UnifierFor(slot.ObjectID()),
func(u values.Unifier) int {
return int(u.Unified().(level.ObjectID))
}, func(value int) string {
return "%d"
}, 0, 871, // TODO: get ID limit of current level
func(newValue int) {
slot.SetObjectID(level.ObjectID(newValue))
onChange()
})
}
func (view *View) createPatchStateControls(readOnly bool, patch archive.PatchState, onChange func()) {
values.RenderUnifiedSliderInt(readOnly,
fmt.Sprintf("Patch Count (%s)", view.indexedName(object.ClassDrug, patch.Index)),
values.UnifierFor(patch.Count()),
func(u values.Unifier) int {
return u.Unified().(int)
}, func(value int) string {
return "%d"
}, 0, 0xFF,
func(newValue int) {
patch.SetCount(newValue)
onChange()
})
}
func (view *View) createInventoryGrenadeControls(readOnly bool, grenade archive.InventoryGrenade, onChange func()) {
values.RenderUnifiedSliderInt(readOnly,
fmt.Sprintf("Grenade Count (%s)", view.indexedName(object.ClassGrenade, grenade.Index)),
values.UnifierFor(grenade.Count()),
func(u values.Unifier) int {
return u.Unified().(int)
}, func(value int) string {
return "%d"
}, 0, 0xFF,
func(newValue int) {
grenade.SetCount(newValue)
onChange()
})
values.RenderUnifiedSliderInt(readOnly, "Timer", values.UnifierFor(grenade.TimerSetting()),
func(u values.Unifier) int {
return int(u.Unified().(archive.GrenadeTimerSetting))
}, func(value int) string {
setting := archive.GrenadeTimerSetting(value)
return fmt.Sprintf("%.01fs -- raw: %%d", setting.InSeconds())
}, 0, archive.GrenadeTimerMaximum,
func(newValue int) {
grenade.SetTimerSetting(archive.GrenadeTimerSetting(newValue))
onChange()
})
}
func (view *View) createInventoryAmmoControls(readOnly bool, ammo archive.InventoryAmmo, onChange func()) {
values.RenderUnifiedSliderInt(readOnly,
fmt.Sprintf("Clip Count (%s)", view.indexedName(object.ClassAmmo, ammo.Index)),
values.UnifierFor(ammo.FullClipCount()),
func(u values.Unifier) int {
return u.Unified().(int)
}, func(value int) string {
return "%d"
}, 0, 0xFF,
func(newValue int) {
ammo.SetFullClipCount(newValue)
onChange()
})
values.RenderUnifiedSliderInt(readOnly,
"Extra rounds",
values.UnifierFor(ammo.ExtraRoundsCount()),
func(u values.Unifier) int {
return u.Unified().(int)
}, func(value int) string {
return "%d"
}, 0, 0xFF,
func(newValue int) {
ammo.SetExtraRoundsCount(newValue)
onChange()
})
}
func (view *View) createInventoryWeaponSlotControls(readOnly bool, slot archive.InventoryWeaponSlot, onChange func()) {
isInUse := slot.IsInUse()
inUseName := fmt.Sprintf("Weapon %d In Use", slot.Index+1)
values.RenderUnifiedCheckboxCombo(readOnly, inUseName, values.UnifierFor(isInUse), func(newUse bool) {
if newUse {
slot.SetInUse(0, 0)
} else {
slot.SetFree()
}
onChange()
})
if !isInUse {
return
}
currentWeaponTriple := slot.Triple()
if imgui.BeginCombo("Type", view.tripleName(currentWeaponTriple)) {
allTypes := view.mod.ObjectProperties().TriplesInClass(currentWeaponTriple.Class)
for _, triple := range allTypes {
if imgui.SelectableV(view.tripleName(triple), triple == currentWeaponTriple, 0, imgui.Vec2{}) {
slot.SetInUse(triple.Subclass, triple.Type)
onChange()
}
}
imgui.EndCombo()
}
isEnergyWeapon := currentWeaponTriple.Subclass >= 4
if isEnergyWeapon {
temperature, chargeAndOverload := slot.WeaponState()
values.RenderUnifiedSliderInt(readOnly, "Temperature", values.UnifierFor(int(temperature)),
func(u values.Unifier) int {
return u.Unified().(int)
}, func(value int) string {
return fmt.Sprintf("%.02f%%%% -- raw: %%d", (float32(value)/float32(lvlobj.EnergyWeaponMaxTemperature))*100)
}, 0x00, lvlobj.EnergyWeaponMaxTemperature,
func(newValue int) {
slot.SetWeaponState(byte(newValue), chargeAndOverload)
onChange()
})
overloadState := chargeAndOverload & 0x80
charge := chargeAndOverload & 0x7F
values.RenderUnifiedCheckboxCombo(readOnly, "Overload", values.UnifierFor(overloadState != 0), func(newOverload bool) {
if newOverload {
slot.SetWeaponState(temperature, 0x80|charge)
} else {
slot.SetWeaponState(temperature, charge)
}
onChange()
})
values.RenderUnifiedSliderInt(readOnly, "Charge", values.UnifierFor(int(charge)),
func(u values.Unifier) int {
return u.Unified().(int)
}, func(value int) string {
return fmt.Sprintf("%.02f%%%% -- raw: %%d", (float32(value)/float32(lvlobj.EnergyWeaponMaxEnergy))*100)
}, 0x00, 0x7F,
func(newValue int) {
slot.SetWeaponState(temperature, overloadState|byte(newValue))
onChange()
})
} else {
rounds, ammoType := slot.WeaponState()
values.RenderUnifiedSliderInt(readOnly, "Rounds", values.UnifierFor(int(rounds)),
func(u values.Unifier) int {
return u.Unified().(int)
}, func(value int) string {
return fmt.Sprintf("%d", value)
}, 0x00, 0xFF,
func(newValue int) {
slot.SetWeaponState(byte(newValue), ammoType)
onChange()
})
values.RenderUnifiedCombo(readOnly, "Ammo Type", values.UnifierFor(int(ammoType)),
func(u values.Unifier) int {
return u.Unified().(int)
},
func(value int) string {
switch value {
case 0:
return "Standard"
case 1:
return "Special"
default:
return fmt.Sprintf("Unknown %d", ammoType)
}
}, 2,
func(newValue int) {
slot.SetWeaponState(rounds, byte(newValue))
onChange()
})
}
}
func (view *View) createVariableControls(readOnly bool, gameState *archive.GameState, onChange func()) {
currentContextIdentifier := view.gameStateService.VariableBaseContext()
if imgui.BeginCombo("Base Context", variableContextList[currentContextIdentifier].title) {
contextIdentifiers := edit.VariableContextIdentifiers()
for _, identifier := range contextIdentifiers {
contextInfo := variableContextList[identifier]
if imgui.SelectableV(contextInfo.title, identifier == currentContextIdentifier, 0, imgui.Vec2{}) {
view.gameStateService.SetVariableBaseContext(identifier)
}
if imgui.IsItemHovered() {
imgui.SetTooltip(contextInfo.info)
}
}
imgui.EndCombo()
}
if imgui.IsItemHovered() {
imgui.SetTooltip("The base context defines to what variables default to\n" +
"in case they are not overridden by the project.")
}
if imgui.Button("Remove All Overrides") {
_ = view.gameStateService.DefaultAllVariables()
}
if imgui.IsItemHovered() {
imgui.SetTooltip("Removes all overrides and their parameters.")
}
if !readOnly {
imgui.SameLine()
if imgui.Button("Reset All") {
for i := 0; i < archive.BooleanVarCount; i++ {
gameState.SetBooleanVar(i, view.gameStateService.BooleanVariable(i).ResetValueBool())
}
for i := 0; i < archive.IntegerVarCount; i++ {
gameState.SetIntegerVar(i, view.gameStateService.IntegerVariable(i).ResetValueInt())
}
onChange()
}
if imgui.IsItemHovered() {
imgui.SetTooltip("Resets all variables to their configured initial value.\n" +
"To reset a single variable, hover over the specific variable and use the context menu.")
}
}
isSavegame := gameState.IsSavegame()
if imgui.TreeNodeV("Boolean Variables", imgui.TreeNodeFlagsFramed) {
intConverter := func(u values.Unifier) int {
bValue := u.Unified().(bool)
if bValue {
return 1
}
return 0
}
defaultNames := map[int]string{0: "False", 1: "True"}
for i := 0; i < archive.BooleanVarCount; i++ {
varIndex := i
info := view.gameStateService.BooleanVariable(varIndex)
varReadOnly := readOnly || (info.Hardcoded && !isSavegame)
varLabel := fmt.Sprintf("Var%03d", varIndex)
varName := fmt.Sprintf("Var%03d: %s", varIndex, info.Name)
varUnifier := values.UnifierFor(gameState.BooleanVar(varIndex))
values.RenderUnifiedCombo(varReadOnly, varName, varUnifier, intConverter,
func(value int) string {
name, found := info.ValueNames[int16(value)]
if found {
return name
}
return defaultNames[value]
},
2,
func(newValue int) {
gameState.SetBooleanVar(varIndex, newValue != 0)
onChange()
})
if imgui.BeginPopupContextItemV(varLabel+"-Popup", 1) {
if imgui.Selectable("Edit info...") {
view.modalStateMachine.SetState(&editBooleanVariableDialog{
machine: view.modalStateMachine,
view: view,
service: view.gameStateService,
index: varIndex,
})
}
if !varReadOnly {
imgui.Separator()
if imgui.Selectable("Reset") {
gameState.SetBooleanVar(varIndex, info.ResetValueBool())
onChange()
}
}
imgui.EndPopup()
} else if imgui.IsItemHovered() && (len(info.Description) > 0) {
imgui.SetTooltip(info.Description)
}
}
imgui.TreePop()
}
if imgui.TreeNodeV("Integer Variables", imgui.TreeNodeFlagsFramed) {
intConverter := func(u values.Unifier) int {
return int(u.Unified().(int16))
}
for i := 0; i < archive.IntegerVarCount; i++ {
varIndex := i
info := view.gameStateService.IntegerVariable(varIndex)
varReadOnly := readOnly || (info.Hardcoded && !isSavegame)
varLabel := fmt.Sprintf("Var%02d", varIndex)
varName := fmt.Sprintf("Var%02d: %s", varIndex, info.Name)
varUnifier := values.UnifierFor(gameState.IntegerVar(varIndex))
changeHandler := func(newValue int) {
gameState.SetIntegerVar(varIndex, int16(newValue))
onChange()
}
switch {
case archive.IsRandomIntegerVariable(varIndex):
values.RenderUnifiedSliderInt(readOnly, varName, varUnifier,
func(u values.Unifier) int { return int(numbers.FromBinaryCodedDecimal(uint16(intConverter(u)))) },
func(value int) string { return "%03d" },
0, 999,
func(newValue int) {
changeHandler(int(numbers.ToBinaryCodedDecimal(uint16(newValue))))
})
case len(info.ValueNames) > 0:
var max int16
for key := range info.ValueNames {
if key > max {
max = key
}
}
values.RenderUnifiedCombo(varReadOnly, varName, varUnifier, intConverter,
func(value int) string {
name, found := info.ValueNames[int16(value)]
if found {
return name
}
return fmt.Sprintf("Unknown %d", value)
},
int(max+1),
changeHandler)
default:
min := math.MinInt16
max := math.MaxInt16
if info.Limits != nil {
min = int(info.Limits.Minimum)
max = int(info.Limits.Maximum)
}
values.RenderUnifiedSliderInt(varReadOnly, varName, varUnifier, intConverter,
func(value int) string {
return "%d"
},
min, max,
changeHandler)
}
if imgui.BeginPopupContextItemV(varLabel+"-Popup", 1) {
if imgui.Selectable("Edit info...") {
view.modalStateMachine.SetState(&editIntegerVariableDialog{
machine: view.modalStateMachine,
view: view,
service: view.gameStateService,
index: varIndex,
})
}
if !varReadOnly {
imgui.Separator()
if imgui.Selectable("Reset") {
gameState.SetIntegerVar(varIndex, info.ResetValueInt())
onChange()
}
}
imgui.EndPopup()
} else if imgui.IsItemHovered() && (len(info.Description) > 0) {
imgui.SetTooltip(info.Description)
}
}
imgui.TreePop()
}
}
func (view *View) createMessageControls(readOnly bool, gameState *archive.GameState, onChange func()) {
view.createMessageControlsFor(readOnly, "EMail",
&view.model.emailIndex, archive.EMailCount, gameState.EMailState, onChange)
view.createMessageControlsFor(readOnly, "Log",
&view.model.logIndex, archive.LogCount, gameState.LogState, onChange)
view.createMessageControlsFor(readOnly, "Fragment",
&view.model.fragmentIndex, archive.FragmentCount, gameState.FragmentState, onChange)
}
func (view *View) createMessageControlsFor(readOnly bool, name string, index *int, limit int,
retriever func(int) archive.MessageState, onChange func()) {
imgui.PushID(name)
gui.StepSliderInt(name+" Index", index, 0, limit-1)
messageState := retriever(*index)
values.RenderUnifiedCheckboxCombo(readOnly, "Received", values.UnifierFor(messageState.Received()),
func(newState bool) {
messageState.SetReceived(newState)
onChange()
})
values.RenderUnifiedCheckboxCombo(readOnly, "Read", values.UnifierFor(messageState.Read()),
func(newState bool) {
messageState.SetRead(newState)
onChange()
})
imgui.PopID()
}
func (view *View) tripleName(triple object.Triple) string {
suffix := hintUnknown
linearIndex := view.mod.ObjectProperties().TripleIndex(triple)
if linearIndex >= 0 {
key := resource.KeyOf(ids.ObjectLongNames, resource.LangDefault, linearIndex)
objName, err := view.textCache.Text(key)
if err == nil {
suffix = singleLineString(objName)
}
}
return triple.String() + ": " + suffix
}
func (view *View) indexedName(class object.Class, index int) string {
startIndex := view.mod.ObjectProperties().TripleIndex(object.TripleFrom(int(class), 0, 0))
if startIndex >= 0 {
key := resource.KeyOf(ids.ObjectLongNames, resource.LangDefault, startIndex+index)
objName, err := view.textCache.Text(key)
if err == nil {
return singleLineString(objName)
}
}
return hintUnknown
}
func singleLineString(str string) string {
return strings.ReplaceAll(strings.ReplaceAll(str, "\r", ""), "\n", "")
}
@@ -0,0 +1,16 @@
package archives
type viewModel struct {
windowOpen bool
restoreFocus bool
selectedLevel int
emailIndex int
logIndex int
fragmentIndex int
}
func freshViewModel() viewModel {
return viewModel{}
}