Huillam Posted July 20, 2016 Share Posted July 20, 2016 I've been trying to alter SitAnywhere to allow user defined hotkeys instead of relying on lesser power.Designing a MCM menu allowing to define hotkey had been easy but so far I've been enable to assign those hotkeys to anything. I've attached two scripts to a dummy quest: The first one appears to be working (it could probably be improved but at least it works). Scriptname SitAnywhere_MCM extends SKI_ConfigBase int SitCrossLegged_OID int Property SitCrossLegged_Key = 22 Auto Hidden Event OnConfigInit() Pages =new string[1] Pages[0] = "Config" EndEvent Event OnPageReset(string page) If (page == "Config") SetCursorFillMode(TOP_TO_BOTTOM) AddHeaderOption("Powers") AddToggleOption("Remove power", False) AddHeaderOption("Hotkeys") SitCrossLegged_OID = AddKeyMapOption("Sit cross-legged", SitCrossLegged_Key) EndIf EndEvent event OnOptionKeyMapChange(int a_option, int KeyCode, string a_conflictControl, string a_conflictName) if (a_option == SitCrossLegged_OID) bool continue = true if (a_conflictControl != "") string msg if (a_conflictName != "") msg = "This key is already mapped to:\n'" + a_conflictControl + "'\n(" + a_conflictName + ")\n\nAre you sure you want to continue?" else msg = "This key is already mapped to:\n'" + a_conflictControl + "'\n\nAre you sure you want to continue?" endIf continue = ShowMessage(msg, true, "$Yes", "$No") endIf if (continue) SitCrossLegged_Key = KeyCode SetKeymapOptionValue(a_option, KeyCode) endIf endIf endEvent Event OnConfigClose() UnregisterForAllKeys() RegisterForKey(SitCrossLegged_Key) EndEvent Right now I'm trying to display a simple debug message when pressing the hotkey (without any success): Scriptname SitAnywhere_KeyPress extends Quest SitAnywhere_MCM Property MCMScript Auto Event OnKeyDown(int KeyCode) if KeyCode == SitAnywhere_MCM.SitCrossLegged_Key Debug.Notification("Hello, World!") endif EndEvent And another version after looking at RND sources (not working either but simply having those files to compile has been a pain): Scriptname SitAnywhere_KeyPress extends ReferenceAlias GlobalVariable Property SitCrossLegged_Key Auto Actor Player Event OnInit() Player = Game.GetPlayer() RegisterForKey(SitCrossLegged_Key.GetValueInt()) EndEvent Event OnKeyDown(int keycode) if keycode == SitCrossLegged_Key Debug.Notification("Hello, World!") endif EndEvent I imagine that I need a way link those two scripts to each others for the second one to recognize SitCrossLegged_Key but I just can't understand how. Link to comment Share on other sites More sharing options...
IsharaMeradin Posted July 20, 2016 Share Posted July 20, 2016 First script is working? Looks like it should. Second script is where? On its own quest or on the same quest as the MCM? If on the same quest as the MCM, it should work provided you properly assigned the property for the MCM script. By being on the same quest as the MCM, it will receive the key events that are registered on the MCM. If it is on a separate quest, you'll have to figure out how to register for the key there. Perhaps in an OnInit, but that would have limitations (see next for why). Your third script would only work if you passed the SitCrossLegged_Key integer of the MCM script into an actual Global that you then accessed on the third script. Also, by registering for the key during the OnInit event of a reference alias you limit the ability to recognize changes to the assigned hot key to those times that the quest is started. Link to comment Share on other sites More sharing options...
Huillam Posted July 20, 2016 Author Share Posted July 20, 2016 Second script is attached to the same dummy quest. I probably failed to assign the property from the mcm. Except I'm not sure what you mean by that. Until now the most complex property I assigned was a name to the first script.And I messed something else because the second script fails to compile: SitAnywhere_KeyPress.psc(6,31): a property cannot be used directly on a type, it must be used on a variable. 6, 31 being SitCrossLegged_Key I've been able to write a non-working but at least able to compile version: Scriptname SitAnywhere_KeyPress extends Quest GlobalVariable Property SitCrossLegged_Key Auto Event OnConfigInit() RegisterForKey(SitCrossLegged_Key.GetValueInt()) EndEvent Event OnKeyDown(int KeyCode) if KeyCode == SitCrossLegged_Key.GetValueInt() Debug.Notification("Hello, World!") endif EndEvent I understand that OnConfigInit is an overall better choice over OnInit but I'm unsure if it's the optimal choice here.I'm still stuck with the GlobalVariable. Trying to delete that line lead to more error messages :SitAnywhere_KeyPress.psc(4,16): variable SitCrossLegged_Key is undefinedSitAnywhere_KeyPress.psc(4,35): none is not a known user-defined typeSitAnywhere_KeyPress.psc(4,1): type mismatch on parameter 1 (did you forget a cast?)SitAnywhere_KeyPress.psc(8,15): variable SitCrossLegged_Key is undefinedSitAnywhere_KeyPress.psc(8,34): none is not a known user-defined typeSitAnywhere_KeyPress.psc(8,12): cannot compare a int to a none (cast missing or types unrelated) Link to comment Share on other sites More sharing options...
IsharaMeradin Posted July 20, 2016 Share Posted July 20, 2016 OnConfigInit() is for use with MCM scripts only. It will do you no good on any other script. Also for that latest script attempt, you can't just delete the property and expect it to compile. That variable is in use throughout the script and has to be defined so that the compiler understands what to do with it. **************************************Try this... Just saw the error on the second script from earlier. You used the actual script name of your MCM script instead of the variable that you assigned it to (see change below). Also, when you click on the properties button and select the MCMScript property you'll get a drop down with each record that the SitAnywhere_MCM script is attached to, select the correct one (if more than one). Scriptname SitAnywhere_KeyPress extends Quest SitAnywhere_MCM Property MCMScript Auto Event OnKeyDown(int KeyCode) if KeyCode == MCMScript.SitCrossLegged_Key ;this is the line that was edited Debug.Notification("Hello, World!") endif EndEvent Link to comment Share on other sites More sharing options...
Huillam Posted July 20, 2016 Author Share Posted July 20, 2016 Ok, understood. I need to call the MCMScript for the hotkey to be recognized. Makes sense.But I'm still unable to display the notification. I tried to save/reload, the hotkey is registered and other mods can display than kind of notification. I'm probably still missing something (something obvious that will make me feel like a moron later on I'm sure of it) in my second script. Link to comment Share on other sites More sharing options...
cdcooley Posted July 20, 2016 Share Posted July 20, 2016 You're over-complicating things by having multiple scripts. You can do all of the work from one. And because I've been thinking about doing something similar for a while now I went ahead and wrote the full script. This version will work as long as you create an MCM quest with the required player alias and loading script then use this as the main quest script. You'll get a valid MCM menu as long as you also have the Sit Anywhere.esp loaded. It's entirely script driven, but the better version would fill the properties in the CK instead of using GetFormFromFile. (Look at the beginning of the OnConfigInit function.) My default key mappings are U for sit cross-legged, no default for sit on ledge, and ESC for the two create powers. Mapped keys remove the matching power and ESC isn't really a valid mapping but it will disable the matching powers anyway. So by default you'll still have the sit on ledge power and only be able to use the sit cross-legged hotkey. And that ModName property really, really should be set from the CK not from the script. In theory it's using the MCM state system but the states are being treated more like variables in this case. You can technically put as many powers or spells as you like into the SitPower property and convert them to hotkey use without any extra coding. But the scripts assume the player already has the matching powers or spells so if not the player will end up getting them for free. (And spells cast through script functions don't play any sounds or animations which isn't a problem for these spells but might be for others.) Scriptname SitAnywhere_MCM extends SKI_ConfigBase {Replace Powers with configured Hotkeys.} Spell[] Property SitPower Auto {Fill with the four powers from the Sit Anywhere mod.} int[] Property SitKeyDefault Auto {Fill with 4 default key values (-1 for unmapped keys, 1 to simply disable the power).} Actor Property PlayerRef Auto {Auto-fill with PlayerRef.} int[] SitKey ;Internal array used to track current configuration of keys. Event OnConfigInit() {Fill initial keycode array from defaults and enable or disable powers as appropriate.} ;; These should be filled in the CK not a script! ModName = "Sit Anywhere" if !SitPower if Game.GetModByName("Sit Anywhere.esp") == 255 return endif SitPower = new Spell[4] SitPower[0] = Game.GetFormFromFile(0x000D68, "Sit Anywhere.esp") as Spell SitPower[1] = Game.GetFormFromFile(0x0048B2, "Sit Anywhere.esp") as Spell SitPower[2] = Game.GetFormFromFile(0x0012CF, "Sit Anywhere.esp") as Spell SitPower[3] = Game.GetFormFromFile(0x0012D3, "Sit Anywhere.esp") as Spell endif if !SitKeyDefault SitKeyDefault = new int[4] SitKeyDefault[0] = 22 SitKeyDefault[1] = -1 SitKeyDefault[2] = 1 SitKeyDefault[3] = 1 endif if !PlayerRef PlayerRef = Game.GetPlayer() endif ;; End of stuff that should have been done in the CK int i int[] junk ; No keys should be registered, but it doesn't hurt to make sure UnregisterForAllKeys() ; SitPower is a manditory property fail if it's not set! if !SitPower || SitPower.length < 1 return endif ; SitKeyDefault is optional so fill it with unmapped keys if it is empty if !SitKeyDefault SitKeyDefault = Utility.CreateIntArray(SitPower.length) i = SitPower.length while i > 0 i -= 1 SitKeyDefault[i] = -1 endwhile endif ; If SitKeyDefault is misconfigured, try to fix it but preserve old values if SitKeyDefault.length != SitPower.length SitKey = Utility.CreateIntArray(SitPower.length) i = SitPower.length while i > 0 i -= 1 if i < SitKeyDefault.length SitKey[i] = SitKeyDefault[i] else SitKey[i] = -1 endif endwhile SitKeyDefault = SitKey SitKey = junk ; empty at this point endif ; Copy defaults into SitKey but preserve any existing values if !SitKey || SitKey.length != SitKeyDefault.length junk = Utility.CreateIntArray(SitKeyDefault.length) i = SitKeyDefault.length while i > 0 i -= 1 if i < SitKey.length junk[i] = SitKey[i] else junk[i] = SitKeyDefault[i] endif endwhile SitKey = junk ; not really junk this time endif ; Register the hotkeys and add or remove the powers to match i = SitPower.length while i > 0 i -= 1 if SitKey[i] > 0 if PlayerRef.HasSpell(SitPower[i]) PlayerRef.RemoveSpell(SitPower[i]) endif elseif !PlayerRef.HasSpell(SitPower[i]) PlayerRef.AddSpell(SitPower[i]) endif RegisterForKey(SitKey[i]) endwhile EndEvent Event OnPageReset(string page) {A short, single page configuration with one entry for each power/hotkey.} SetCursorFillMode(TOP_TO_BOTTOM) OnConfigInit() ; Just to make absolutely sure everything is configured it possible if !SitPower || SitPower.length < 1 return ; Something is terribly wrong! endif int i = 0 while i < SitPower.length AddKeyMapOptionST(i + "KEY", SitPower[i].GetName() + " Hotkey", SitKey[i], OPTION_FLAG_WITH_UNMAP) i += 1 endwhile EndEvent Event OnHighlightST() {Every option gets the same helper info text at the bottom of the screen.} SetInfoText("Setting a hotkey disables the matching power.\nUnmapping the hotkey restores the power.\nMapping to ESC will not create a hotkey but will disable the power.") EndEvent Event OnDefaultST() {Default state is retreived from the array based on the current state.} int keyCode = SitKeyDefault[GetState() as int] ;if SitKey.Find(keyCode) > -1 ; ShowMessage("This key is already mapped to:\n'" + GetCustomControl(keyCode) + "'\n(Sit Anywhere)\nCan't restore the default right now.", false, "$OK") ; return ;endif OnKeyMapChangeST(keyCode, GetCustomControl(keyCode), "Sit Anywhere") ; unmapped EndEvent Event OnKeyMapChangeST(int keyCode, string conflictControl, string conflictName) {Make sure the key is valid then record it and unregister/register keys and add/remove power as needed.} if keyCode > 1 if conflictName conflictName = "(" + conflictName + ")" else conflictName = "" endif if conflictControl if SitKey.Find(keyCode) > -1 ShowMessage("This key is already mapped to:\n'" + conflictControl + "'\n" + conflictName+ "\nYou can't map the same key twice.", false, "$OK") return elseif !ShowMessage("This key is already mapped to:\n'" + conflictControl + "'\n" + conflictName+ "\nAre you sure you want to continue?", true, "$Yes", "$No") return endif endIf elseif keyCode == 1 if !ShowMessage("Mapping a key to ESC will disable the power without creating a hotkey.\nAre you sure you wan tot continue?", true, "$Yes", "$No") return endif endif int i = GetState() as int if SitKey[i] > 1 UnregisterForKey(SitKey[i]) endif SitKey[i] = keyCode if keyCode > 1 RegisterForKey(keyCode) endif if keyCode > 0 if PlayerRef.HasSpell(SitPower[i]) PlayerRef.RemoveSpell(SitPower[i]) endif elseif !PlayerRef.HasSpell(SitPower[i]) PlayerRef.AddSpell(SitPower[i]) endif SetKeyMapOptionValueST(keyCode) EndEvent string function GetCustomControl(int keyCode) {Allows reporting previously mapped keys in this mod for itself and other MCM pages.} int i = SitKey.Find(keyCode) if i < 0 || SitKey[i] == 1 ; ESC doesn't count return "" else return SitPower[i].GetName() + " Hotkey" endif endFunction Event OnKeyDown(int code) {The real work of making the hotkey work like the matching powers is very simple.} SitPower[SitKey.Find(code)].Cast(PlayerRef) EndEvent Link to comment Share on other sites More sharing options...
Huillam Posted July 20, 2016 Author Share Posted July 20, 2016 That script is a lot more complex (or at least bigger) that anything I imagined and I'll probably (more than probably in fact) need some time to fully understand it. Anyway thanks to the both of you. :smile: Link to comment Share on other sites More sharing options...
Huillam Posted July 20, 2016 Author Share Posted July 20, 2016 That's probably some stupid questions (and assertions) but meh. Regarding the things that are supposed to be filled in the CK: ModName and PlayerRef were easy enough and should be ok. SitPower use the FormID of the spells from the mod. For SitKeyDefault I'm a bit puzzled: It follows the same order that than SitPower but is that enough for key X to be assigned by default to spell[0] and key Y to spell[1] or is there something I missed? Is GetModByName a property that I'm supposed to add to my script? Am I supposed to add that block of code to my script in addition to the properties? Obviously yes, as fillind the property and pasting all your code makes things work flawlessly but as I said: Stupid questions. Link to comment Share on other sites More sharing options...
cdcooley Posted July 20, 2016 Share Posted July 20, 2016 It's long because I put error and safety checks for everything I could think of to prevent problems. There's even that commented out section in OnDefaultST that gives a slightly different message if you try to reset a key to a default that has been remapped to another key in the mod. To fill SitPower in the CK you first need to ESMify Sit Anywhere.esp then load up it and this mod in the CK. When you go to fill the properties on SitPower it's an array so you have to add 4 entries and select each of the powers individually. It's tedious compared to auto-fill but the script would be even longer if you used 4 individual properties. (Don't forget to ESPify Sit Anywhere.esp when you're finished.) SitKeyDefault is also an array and you should add the same number of entries as powers. And yes, order matters to match them up. SitKeyDefault[2] will be the key for SitPower[2], etc. The somewhat complicated code in OnConfigInit makes sure that even if you fill SitKeyDefault incorrectly (having too many or too few entries) something sensible will happen when you go to use the mod. GetModByName is one of the SKSE functions. If you fill the properties correctly you can completely remove that first chunk of code from OnConfigInit. Or you can leave it because if the properties are filled in the CK then the if statements will keep that section from doing anything anyway. (Except for the line about setting ModName where I forgot to wrap it in and "If !ModName" check like I did for the other 3 properties. Link to comment Share on other sites More sharing options...
Huillam Posted July 20, 2016 Author Share Posted July 20, 2016 Thanks to you things make sense to me. There's still one last issue: the keys need to be deactivated in some occasion (mostly while menus are opened).I came with this (slightly altered piece of code from frostfall):Event OnKeyDown(int code) {The real work of making the hotkey work like the matching powers is very simple.} if UI.IsMenuOpen("Console") || UI.IsMenuOpen("Book Menu") || UI.IsMenuOpen("BarterMenu") || UI.IsMenuOpen("ContainerMenu") || UI.IsMenuOpen("Crafting Menu") || UI.IsMenuOpen("Dialogue Menu") || UI.IsMenuOpen("FavoritesMenu") || UI.IsMenuOpen("InventoryMenu") || UI.IsMenuOpen("Journal Menu") || UI.IsMenuOpen("Lockpicking Menu") || UI.IsMenuOpen("MagicMenu") || UI.IsMenuOpen("MapMenu") || UI.IsMenuOpen("MessageBoxMenu") || UI.IsMenuOpen("Sleep/Wait Menu") || UI.IsMenuOpen("StatsMenu") return else SitPower[SitKey.Find(code)].Cast(PlayerRef) endif EndEventIs this correct? I'm asking because Chesko's code looks like this:Event OnKeyDown(int keyCode) if UI.IsMenuOpen("Console") || UI.IsMenuOpen("Book Menu") || UI.IsMenuOpen("BarterMenu") || UI.IsMenuOpen("ContainerMenu") || UI.IsMenuOpen("Crafting Menu") || UI.IsMenuOpen("Dialogue Menu") || UI.IsMenuOpen("FavoritesMenu") || UI.IsMenuOpen("InventoryMenu") || UI.IsMenuOpen("Journal Menu") || UI.IsMenuOpen("Lockpicking Menu") || UI.IsMenuOpen("MagicMenu") || UI.IsMenuOpen("MapMenu") || UI.IsMenuOpen("MessageBoxMenu") || UI.IsMenuOpen("Sleep/Wait Menu") || UI.IsMenuOpen("StatsMenu") return endif if keyCode == _Frost_HotkeyWeathersense.GetValueInt() _Frost_Weathersense_Spell.Cast(PlayerRef) endif EndEventI've tried to stay as close as possible to his code (contrary to me he probably knows what he's doing) but using the same syntax ("if, endif, if, endif" instead of "if, else, endif") but then the script failed to compile. He uses int keycode instead of int code which may explain everything. Link to comment Share on other sites More sharing options...
Recommended Posts