Fractal Softworks Forum

Please login or register.

Login with username, password and session length
Advanced search  

News:

Starsector 0.97a is out! (02/02/24); In-development patch notes for Starsector 0.98a (2/8/25)

Author Topic: [Modding Request] Adding dialog options at index through scripts  (Read 491 times)

presidentmattdamon

  • Commander
  • ***
  • Posts: 249
    • View Profile

right now i've got an every frame script to add a dialog option if the current dialog's plugin is a FleetInteractionDialogPluginImpl. adding an option the normal way is by calling OptionPanelAPI.addOption, which adds the option to the bottom of the list, almost invariably below any "Leave" option which just looks really awkward.

to solve this, AtlanticAccent and i used the powers of reflection. long code snippet follows:

Spoiler
Code
fun OptionPanelAPI.addOptionAtIndexWithHandler(
    index: Int,
    text: String,
    data: Any,
    color: Color = Misc.getBasePlayerColor(),
    tooltip: String?,
    delegate: () -> Unit
) {
    this.addOption(
        text,
        data,
        color,
        tooltip
    )

    val originalOptionMap = ReflectionUtils.invoke("getButtonToItemMap", this) as MutableMap<Any?, Any?>
    val copiedOptionMap: MutableMap<Any?, Any?> = linkedMapOf()
    originalOptionMap.entries.forEach { (val1, val2) ->
        copiedOptionMap.put(val1, val2)
    }

    val originalOptions = this.savedOptionList
    var optionAdded = false
    val newOptions: MutableList<Any?> = mutableListOf()
    for (i in 0 until originalOptions.size - 1) {
        if (i == index) {
            newOptions.add(originalOptions.last())
            optionAdded = true
        }
        newOptions.add(originalOptions[i])
    }

    if (!optionAdded) {
        newOptions.add(originalOptions.last())
    }

    this.restoreSavedOptions(newOptions)
    resetOptionHotkeys(copiedOptionMap, this)
    resetOptionConfirmationDialogs(copiedOptionMap, this)

    this.addOptionConfirmation(
        data, AutoClosingOptionDelegate(delegate)
    )
}

fun resetOptionHotkeys(oldOptions: Map<Any?, Any?>, options: OptionPanelAPI) {
    val dataMethodName = ReflectionUtils.getMethodOfReturnType(oldOptions.values.first()!!, Object::class.java)!!
    val optionMap = ReflectionUtils.invoke("getButtonToItemMap", options) as MutableMap<Any?, Any?>
    for (newOption in optionMap.values) {
        newOption!!
        val newOptionData = ReflectionUtils.invoke(dataMethodName, newOption)
        oldOptions
            .filter { (_, optionObj) ->
                ReflectionUtils.invoke(dataMethodName, optionObj!!) == newOptionData
            }
            .entries
            .first()
            .let { (keyObj, optionObj) ->
                keyObj!!
                optionObj!!
                val firstArgClassOfAltShortcut = ReflectionUtils.getMethodArguments("setAltShortcut", keyObj)!![0]
                val optionHandlingScriptField = ReflectionUtils.findFieldWithMethodName(keyObj, "focusLost")!!
                val optionHandlingScriptObj = optionHandlingScriptField.get(keyObj)!!
                val keyHandlerFields =
                    ReflectionUtils.findFieldsOfType(optionHandlingScriptObj, firstArgClassOfAltShortcut)
                keyHandlerFields
                    .mapNotNull { it.get(optionHandlingScriptObj) }
                    .forEach { keyHandlerObj ->
                        val keyField = ReflectionUtils.findFieldsOfType(keyHandlerObj, Int::class.java)[0]
                        val actualKey = keyField.get(keyHandlerObj) as Int
                        if (!(Keyboard.KEY_1..Keyboard.KEY_9).contains(actualKey)) {
                            options.setShortcut(
                                newOptionData,
                                actualKey,
                                false,
                                false,
                                false,
                                true
                            )
                        }
                    }

            }
    }
}

fun resetOptionConfirmationDialogs(oldOptions: Map<Any?, Any?>, options: OptionPanelAPI) {
    val nameMethodName = ReflectionUtils.getMethodOfReturnType(oldOptions.values.first()!!, String::class.java)!!
    val dataMethodName = ReflectionUtils.getMethodOfReturnType(oldOptions.values.first()!!, Object::class.java)!!
    val optionMap = ReflectionUtils.invoke("getButtonToItemMap", options) as MutableMap<Any?, Any?>
    for (newOption in optionMap.values) {
        newOption!!
        val newOptionName = ReflectionUtils.invoke(nameMethodName, newOption)
        val newOptionData = ReflectionUtils.invoke(dataMethodName, newOption)
        oldOptions
            .filter { (_, optionObj) ->
                ReflectionUtils.invoke(dataMethodName, optionObj!!) == newOptionData
            }
            .entries
            .first()
            .let { (keyObj, optionObj) ->
                optionObj!!
                val confirmationStringFields = ReflectionUtils.findFieldsOfType(optionObj, String::class.java)
                    .map { it.get(optionObj) as String? }
                    .filterNotNull()
                    .filter { it != newOptionName }
                if (confirmationStringFields.size >= 3) {
                    val confirmationStringText: String
                    val confirmationStringYes: String
                    val confirmationStringNo: String
                    if (confirmationStringFields.size == 4) {
                        confirmationStringText = confirmationStringFields[1]
                        confirmationStringYes = confirmationStringFields[2]
                        confirmationStringNo = confirmationStringFields[3]
                    } else {
                        confirmationStringText = confirmationStringFields[0]
                        confirmationStringYes = confirmationStringFields[1]
                        confirmationStringNo = confirmationStringFields[2]
                    }
                    options.addOptionConfirmation(
                        newOptionData,
                        confirmationStringText,
                        confirmationStringYes,
                        confirmationStringNo
                    )
                }

                val confirmationStoryField =
                    ReflectionUtils.findFieldsOfType(optionObj, StoryPointActionDelegate::class.java)[0]
                val confirmationStoryDelegate = confirmationStoryField.get(optionObj) as StoryPointActionDelegate?
                if (confirmationStoryDelegate != null) {
                    options.addOptionConfirmation(newOptionData, confirmationStoryDelegate)
                }
            }
    }
}

//Thanks AtlanticAccent
open class AutoClosingOptionDelegate(val block: () -> Unit) :
    BaseStoryPointActionDelegate() {

    override fun getRequiredStoryPoints(): Int = 0

    override fun withSPInfo(): Boolean = false

    override fun createDescription(info: TooltipMakerAPI) {
        block()
        DialogEFScript.hack = info
    }

    override fun getLogText(): String =
        "Why are we still here? Just to suffer? Every night, I can feel my leg... And my arm... even my fingers... The body I've lost... the comrades I've lost... won't stop hurting... It's like they're all still there. You feel it, too, don't you? I'm gonna make them give back our past!"
}
[close]

i apologize that it's in Kotlin. converting it to java would require another 50 or so lines. to put it shortly, a BaseStoryPointActionDelegate can be added to the option to run a piece of code. then use reflection to hit the cancel button so the story action dialog doesn't show up instantly.

the option is placed there using reflection. in order to preserve confirmation delegates and hotkeys, i have to add the option, get the internal hotkey-option map using reflection, remove all options, add all options in the menu back, then use the reflected map to restore the hotkeys and delegates using more reflection.

this isn't maintainable in the slightest. if you change anything about how the OptionPanelAPI works, this will probably break. it is the absolute cleanest way to present an option in any dialog, while preserving hotkeys, confirmation dialogs, and story point actions, while allowing the option to be placed at any index (namely, above any leave button.)

can this be improved somehow?
Logged

presidentmattdamon

  • Commander
  • ***
  • Posts: 249
    • View Profile
Re: [Modding Request] Adding dialog options at index through scripts
« Reply #1 on: January 21, 2024, 10:47:34 AM »

AtlanticAccent's version of the same script is a lot shorter:

Spoiler
Code
val options = dialog.optionPanel

options.addOption(
    "Reassign captains",
    pc_AutoClosingOptionDelegate.OPTION_ID,
    "Last minute reassignment of captains to ships"
)

val originalOptions = options.savedOptionList

val newOptions = originalOptions.toMutableList()
val added = newOptions.removeLast()
newOptions.add(1, added)

val oldOptionMap = (ReflectionUtils.invoke(
    "getButtonToItemMap",
    options
) as Map<Any?, Any?>).toMap()

options.restoreSavedOptions(newOptions)

val idMethodName = ReflectionUtils.getMethodOfReturnType(originalOptions.first()!!, "".javaClass)!!
fun Any?.id() = ReflectionUtils.invoke(idMethodName, this!!)
val originalMap = originalOptions.associateBy { it.id() }
val optionMap = ReflectionUtils.invoke("getButtonToItemMap", options) as MutableMap<Any?, Any?>
for (key in optionMap.keys) {
    val item = optionMap[key]!!
    val id = item.id()
    val newItem = originalMap[id]
    optionMap[key] = newItem

    val oldKey = oldOptionMap.entries.first { (_, entry) -> entry.id() == id }.key!!

    val firstArgClassOfAltShortcut = ReflectionUtils.getMethodArguments("setAltShortcut", oldKey)!![0]
    val optionHandlingScriptField = ReflectionUtils.findFieldWithMethodName(oldKey, "focusLost")!!
    val optionHandlingScriptObj = optionHandlingScriptField.get(oldKey)!!
    val keyHandlerFields =
        ReflectionUtils.findFieldsOfType(optionHandlingScriptObj, firstArgClassOfAltShortcut)
    keyHandlerFields
        .mapNotNull { it.get(optionHandlingScriptObj) }
        .forEach { keyHandlerObj ->
            val keyField = ReflectionUtils.findFieldsOfType(keyHandlerObj, Int::class.java)[0]
            val actualKey = keyField.get(keyHandlerObj) as Int
            if (!(Keyboard.KEY_1..Keyboard.KEY_9).contains(actualKey)) {
                ReflectionUtils.invoke("setAltShortcut", key!!, keyHandlerObj, true)
            }
        }
}

options.addOptionConfirmation(
    pc_AutoClosingOptionDelegate.OPTION_ID, pc_ReassignOfficerOptionDelegate(dialog)
)
[/spoiler]

this is because he uses the keys from the original ButtonToItemMap directly rather than creating new ones, which preserves any confirmation dialogs. the result is a lot shorter.
[close]
Logged

AtlanticAccent

  • Lieutenant
  • **
  • Posts: 80
    • View Profile
Re: [Modding Request] Adding dialog options at index through scripts
« Reply #2 on: January 22, 2024, 09:36:31 AM »

I think I've expressed this elsewhere, but a minimal subset of this I'd like to see is renaming StoryPointActionDelegate and adding methods to it like
Code
public bool hasConfirmationDialog
and
Code
public void onSelected
.

Code
hasConfirmationDialog
would stop the game from displaying the confirmation popup that story point options currently always display.
Code
onSelected
would allow running some arbitrary code when the option the delegate is attached to is chosen/clicked.

These two would allow us to a) stop using the
Code
createDescription
method of the delegate to run code, and b) remove the need for our multi-frame workaround to dismissing the stock story-point-spending confirmation dialog.

Though, to be honest, it would be nice if the existing exposed API method
Code
restoreSavedOptions
were to be more "complete" and also restore delegates and hotkeys - this is an issue even when not trying to change the order of the options list.
Logged