Darklocq Posted April 4, 2019 Share Posted April 4, 2019 (edited) How would one tweak Jaxons Lights Please to use mod-provided spells? I have a lighting spell enhancement mod that provides stronger versions of Magelight and Candlelight. I just don't understand Papyrus's poorly documented syntax well enough to pull this off, simple as it seems. No matter what I try, I get errors relating to variable/property types ("did you forget a cast?", "not a known user-defined type", "cannot cast a string to a form", "not a known user-defined type", and so on. I can't believe it's this hard to tell Papyrus "if the player has Magelight3, then use that spell ID, not Magelight". Argh. Here's the latest version I was working on, but this is about the 20th variant I've tried over the course of three hours. Scriptname JaxonzLightsPlease extends Quest {Simple utility to improve Candlelight and Magelight casting} Bool bCandleLightOn = false GlobalVariable Property giCandleLightHotkey Auto GlobalVariable Property gfTimeScale Auto GlobalVariable Property giMageLightHotkey Auto Actor Property actPlayer Auto Spell Property splCandleLight Auto Spell Property splMageLight Auto Spell Property Candlelight5 Auto Spell Property Candlelight4 Auto Spell Property Candlelight3 Auto Spell Property Candlelight2 Auto Spell Property Magelight5 Auto Spell Property Magelight4 Auto Spell Property Magelight3 Auto Spell Property Magelight2 Auto Sound Property sndFail Auto String CLSpellID String MLSpellID Event OnInit() ; Does the player have better lights from MoreMagicLight.esp? if (actPlayer.HasSpell(Candlelight5)) CLSpellID = Candlelight5 elseif (actPlayer.HasSpell(Candlelight4)) CLSpellID = Candlelight4 elseif (actPlayer.HasSpell(Candlelight3)) CLSpellID = Candlelight3 elseif (actPlayer.HasSpell(Candlelight2)) CLSpellID = Candlelight2 elseif (actPlayer.HasSpell(splCandlelight)) CLSpellID = splCandlelight endif if (actPlayer.HasSpell(Magelight5)) MLSpellID = Magelight5 elseif (actPlayer.HasSpell(Magelight4)) MLSpellID = Magelight4 elseif (actPlayer.HasSpell(Magelight3)) MLSpellID = Magelight3 elseif (actPlayer.HasSpell(Magelight2)) MLSpellID = Magelight2 elseif (actPlayer.HasSpell(splMagelight)) MLSpellID = splMagelight endif OnSettingsChanged() EndEvent Function OnSettingsChanged() ;update key mappings; called by MCM menu UnregisterForAllKeys() ;register keystroke callbacks RegisterForKey(giCandleLightHotkey.GetValueInt()) RegisterForKey(giMageLightHotkey.GetValueInt()) EndFunction Event OnKeyDown(Int KeyCode) if !Utility.IsInMenuMode() ;only pay attention to hotkeys in game mode ;see if there is an object grabbed If KeyCode == giCandleLightHotkey.GetValueInt() If actPlayer.HasSpell(CLSpellID) bCandleLightOn = !bCandleLightOn if bCandleLightOn CastCandleLight() Else actPlayer.DispelSpell(CLSpellID) EndIf Else Debug.Notification("Unable to cast Candlelight because you don't know that spell") EndIf ElseIf KeyCode == giMageLightHotkey.GetValueInt() If actPlayer.HasSpell(MLSpellID) If actPlayer.GetActorValue("Magicka") >= MLSpellID.GetEffectiveMagickaCost(actPlayer) actPlayer.DamageActorValue("Magicka", MLSpellID.GetEffectiveMagickaCost(actPlayer)) MLSpellID.Cast(actPlayer) Else sndFail.Play(actPlayer as ObjectReference) ;insufficient magicka sound EndIf Else Debug.Notification("Unable to cast Magelight because you don't know that spell") EndIf EndIf EndIf EndEvent Event OnUpdate() ;Event OnUpdateGameTime() if bCandleLightOn CastCandleLight() Else ; UnregisterForUpdateGameTime() EndIf EndEvent Function CastCandleLight() if (actPlayer.GetActorValue("Magicka") >= CLSpellID.GetEffectiveMagickaCost(actPlayer)) actPlayer.DamageActorValue("Magicka", CLSpellID.GetEffectiveMagickaCost(actPlayer)) CLSPellID.Cast(actPlayer) ; RegisterForSingleUpdateGameTime((CLSpellID.GetNthEffectDuration(0) as float) * 0.00028 * gfTimeScale.GetValue() ) ;convert spell duration seconds to game hours RegisterForSingleUpdate((splCandleLight.GetNthEffectDuration(0) - 1) as float) Else sndFail.Play(actPlayer as ObjectReference) ;insufficient magicka sound Debug.Notification("Insufficient magicka to renew Candlelight; retry in 5 seconds ...") RegisterForSingleUpdate(5.0) EndIf EndFunction Here's the original script: Scriptname JaxonzLightsPlease extends Quest {Simple utility to improve Candlelight and Magelight casting} Bool bCandleLightOn = false GlobalVariable Property giCandleLightHotkey Auto GlobalVariable Property gfTimeScale Auto GlobalVariable Property giMageLightHotkey Auto Actor Property actPlayer Auto Spell Property splCandleLight Auto Spell Property splMageLight Auto Sound Property sndFail Auto Event OnInit() OnSettingsChanged() EndEvent Function OnSettingsChanged() ;update key mappings; called by MCM menu UnregisterForAllKeys() ;register keystroke callbacks RegisterForKey(giCandleLightHotkey.GetValueInt()) RegisterForKey(giMageLightHotkey.GetValueInt()) EndFunction Event OnKeyDown(Int KeyCode) if !Utility.IsInMenuMode() ;only pay attention to hotkeys in game mode ;see if there is an object grabbed If KeyCode == giCandleLightHotkey.GetValueInt() If actPlayer.HasSpell(splCandleLight) bCandleLightOn = !bCandleLightOn if bCandleLightOn CastCandleLight() Else actPlayer.DispelSpell(splCandleLight) EndIf Else Debug.Notification("Unable to cast Candlelight because you don't know that spell") EndIf ElseIf KeyCode == giMageLightHotkey.GetValueInt() If actPlayer.HasSpell(splMageLight) If actPlayer.GetActorValue("Magicka") >= splMageLight.GetEffectiveMagickaCost(actPlayer) actPlayer.DamageActorValue("Magicka", splMageLight.GetEffectiveMagickaCost(actPlayer)) splMageLight.Cast(actPlayer) Else sndFail.Play(actPlayer as ObjectReference) ;insufficient magicka sound EndIf Else Debug.Notification("Unable to cast Magelight because you don't know that spell") EndIf EndIf EndIf EndEvent Event OnUpdate() ;Event OnUpdateGameTime() if bCandleLightOn CastCandleLight() Else ; UnregisterForUpdateGameTime() EndIf EndEvent Function CastCandleLight() if (actPlayer.GetActorValue("Magicka") >= splCandleLight.GetEffectiveMagickaCost(actPlayer)) actPlayer.DamageActorValue("Magicka", splCandleLight.GetEffectiveMagickaCost(actPlayer)) splCandleLight.Cast(actPlayer) ; RegisterForSingleUpdateGameTime((splCandleLight.GetNthEffectDuration(0) as float) * 0.00028 * gfTimeScale.GetValue() ) ;convert spell duration seconds to game hours RegisterForSingleUpdate((splCandleLight.GetNthEffectDuration(0) - 1) as float) Else sndFail.Play(actPlayer as ObjectReference) ;insufficient magicka sound Debug.Notification("Insufficient magicka to renew Candlelight, retry in 5 seconds...") RegisterForSingleUpdate(5.0) EndIf EndFunction Aside: It's unclear to my why the original script (and this one, in the parts of it I've retained) is calling vanilla Magelight as splMageLight, since that's not its ID (and the mod doesn't add one by that name; it adds no spells or effects at all). I assume this is some kind of special syntax. Another mod I looked at, which adds a spell to the player's spell list the first time the game is loaded with that mod, and it calls its spell by its actual EditorID name in the "Spell Property FooBarBaz Auto" line; it doesn't use "Spell Property splFooBarBaz Auto". Edited April 4, 2019 by Darklocq Link to comment Share on other sites More sharing options...
foamyesque Posted April 4, 2019 Share Posted April 4, 2019 You're suffering from some misconceptions here, fundamental ones. Firstly, what variables and properties are named has exactly zero required connection to what they represent. Consider the case of storing a number: You wouldn't name the variable that holds '5' as "Five", right? So, splFooBar and FooBar can both have the Spell BarFoo or anything else of the type they're declared as in them. For Properties, which is usually how you tell Papyrus what game objects you need it to talk to, you can see what they are set to by hitting the "Edit Properties" menu. It is usual practice to name things in a descriptive way that can be understood by someone looking at the code, so often they closely match an editor ID, but it's by no means always true. Second is that Papyrus is a fairly strongly typed language. Every variable and property has a specific kind of thing it can store: integers, floats, Forms, Actors, whatever. You declare this explicitly when you create any of them. Some, but not all, types can be cast between each other. Integers can be converted to and from floating point numbers, anything can be converted to a string (but the reverse is not true), script objects can be converted along their inheritance tree (always up; down if and only if they are also the subtype you cast to. For example, an Actor can be cast to an ObjectReference can be cast to a Form, but a Form can only be cast to an ObjectReference is it is an actual thing in the game world, with position data and so on. You can find a table here:https://www.creationkit.com/index.php?title=Category:Script_Objects) So what's happening in your original script is that, for example, when you say, "actPlayer.HasSpell(CLSpellID)", you're asking if the player has the spell "<arbitrary sequence of letters and characters>". As far as the compiler is concerned this is a nonsense, like asking about the taste of five. Fortunately the coding to solve your problem simplifies things. You don't need to convert to strings at all! (Or back!) The logic flow in the Jaxon mod is to have a specific hotkey that then uses Papyrus scripting to simulate casting a spell through the game systems, because those game systems won't let it quite do what is desired. All you have to do is change what spell is being cast. Consider something like this: Scriptname JaxonzLightsPleaseEXT extends JaxonzLightsPlease {Simple utility to improve Candlelight and Magelight casting} FormList Property CandleLightList Auto FormList Property MageLightList Auto Event OnKeyDown(Int KeyCode) if !Utility.IsInMenuMode() ;only pay attention to hotkeys in game mode if KeyCode == giCandleLightHotkey.GetValueInt() Spell testSpell = GetBestKnownSpell(CandleLightList) if testSpell splCandleLight = testSpell endif elseif KeyCode == giMageLightHotkey.GetValueInt() Spell testSpell = GetBestKnownSpell(MageLightList) if testSpell splMageLight = testSpell endif endif parent.OnKeyDown(Int KeyCode) EndIf EndEvent ;assumes formlist is organized from least(0) to best(n) Function GetBestKnownSpell(FormList akSpellList) int i = akSpellList.GetSize() Spell bestSpell = none while i > 0 && !bestSpell i-=1 Spell testSpell = akSpellList.GetAt(i) as Spell if testSpell if actPlayer.HasSpell(testSpell) return testSpell endif endif endwhile return none EndFunction When a Papyrus script extends another it has access to all the code and functions written in the first, and the properties (though not the variables). So what's happening here is I have, in essence, inserted a preprocessor that pulls the best spell out of the appropriate formlist, and saves that (if the player has it) into the property that the rest of the Jaxon script accesses in its casting routines and logic. Other methods for doing the same thing -- e.g. just directly editing the code of the original script -- exist, depending on implementation details. Link to comment Share on other sites More sharing options...
Darklocq Posted April 11, 2019 Author Share Posted April 11, 2019 Thanks, foamyesque, that was very helpful. Link to comment Share on other sites More sharing options...
Darklocq Posted April 11, 2019 Author Share Posted April 11, 2019 (edited) [blanking comment - the forum software keeps mangling it; will try a new one.] Edited April 11, 2019 by Darklocq Link to comment Share on other sites More sharing options...
Darklocq Posted April 11, 2019 Author Share Posted April 11, 2019 This doesn't compile, unfortunately. I first got:* (22, 24) Mismatched input 'int', expecting RPAREN* (22, 35) Required (...)+ loop did not match anything at input ')' After fixing "parent.OnKeyDown(Int KeyCode)" to read just "parent.OnKeyDown()", it got past that problem, but had other errors:* (22, 11) Argument keycode is not specified and has no default value* (36, 16) Cannot return a spell from getbestknownspell, the function does not return a value I tried integrating the gist of this into the original script (I wasn't actually quite sure what to do with a separate one): Scriptname JaxonzLightsPlease extends Quest {Simple utility to improve Candlelight and Magelight casting} Bool bCandleLightOn = false GlobalVariable Property giCandleLightHotkey Auto GlobalVariable Property gfTimeScale Auto GlobalVariable Property giMageLightHotkey Auto Actor Property actPlayer Auto Spell Property splCandleLight Auto Spell Property splMageLight Auto Sound Property sndFail Auto FormList Property CandleLightList Auto FormList Property MageLightList Auto Event OnInit() OnSettingsChanged() EndEvent Function OnSettingsChanged() ;update key mappings; called by MCM menu UnregisterForAllKeys() ;register keystroke callbacks RegisterForKey(giCandleLightHotkey.GetValueInt()) RegisterForKey(giMageLightHotkey.GetValueInt()) EndFunction Event OnKeyDown(Int KeyCode) if !Utility.IsInMenuMode() ;only pay attention to hotkeys in game mode ;see if there is an object grabbed If KeyCode == giCandleLightHotkey.GetValueInt() Spell testSpell = GetBestKnownSpell(CandleLightList) If testSpell splCandleLight = testSpell EndIf If actPlayer.HasSpell(splCandleLight) bCandleLightOn = !bCandleLightOn If bCandleLightOn CastCandleLight() Else actPlayer.DispelSpell(splCandleLight) EndIf Else Debug.Notification("Unable to cast Candlelight because you don't know that spell") EndIf ElseIf KeyCode == giMageLightHotkey.GetValueInt() Spell testSpell = GetBestKnownSpell(MageLightList) If testSpell splMageLight = testSpell EndIf If actPlayer.HasSpell(splMageLight) If actPlayer.GetActorValue("Magicka") >= splMageLight.GetEffectiveMagickaCost(actPlayer) actPlayer.DamageActorValue("Magicka", splMageLight.GetEffectiveMagickaCost(actPlayer)) splMageLight.Cast(actPlayer) Else sndFail.Play(actPlayer as ObjectReference) ;insufficient magicka sound EndIf Else Debug.Notification("Unable to cast Magelight because you don't know that spell") EndIf EndIf EndIf EndEvent Event OnUpdate() ;Event OnUpdateGameTime() if bCandleLightOn CastCandleLight() Else ; UnregisterForUpdateGameTime() EndIf EndEvent Function GetBestKnownSpell(FormList akSpellList) ;assumes formlist is organized from least(0) to best(n) Int i = akSpellList.GetSize() Spell bestSpell = none While i > 0 && !bestSpell i-=1 Spell testSpell = akSpellList.GetAt(i) as Spell If testSpell If actPlayer.HasSpell(testSpell) Return testSpell EndIf EndIf EndWhile Return none EndFunction Function CastCandleLight() If (actPlayer.GetActorValue("Magicka") >= splCandleLight.GetEffectiveMagickaCost(actPlayer)) actPlayer.DamageActorValue("Magicka", splCandleLight.GetEffectiveMagickaCost(actPlayer)) splCandleLight.Cast(actPlayer) ; RegisterForSingleUpdateGameTime((splCandleLight.GetNthEffectDuration(0) as float) * 0.00028 * gfTimeScale.GetValue() ) ;convert spell duration seconds to game hours RegisterForSingleUpdate((splCandleLight.GetNthEffectDuration(0) - 1) as float) Else sndFail.Play(actPlayer as ObjectReference) ;insufficient magicka sound Debug.Notification("Insufficient magicka to renew Candlelight, retry in 5 seconds...") RegisterForSingleUpdate(5.0) EndIf EndFunction Link to comment Share on other sites More sharing options...
Darklocq Posted April 11, 2019 Author Share Posted April 11, 2019 (Cont'd from above (my post kept truncating at the /spoiler:) But I still got the "Cannot return a spell from getbestknownspell, the function does not return a value" error despite it clearly returning a value. I searched around, and it seems that functions in this language default to Int as the return type, but must be explicitly defined as another if this is not the intent. I could not find a list of return type, but guessed blindly (from Int) that it's the entire list of things like Bool, Spell, Actor, etc., so I tried "Spell Function GetBestKnownSpell(FormList akSpellList)" in your code, and that got rid of that error, though the one about "Argument keycode is not specified and has no default value" remained. I tried the same fix in my merged version, and it compiled! Now, I'm not sure if doing it as I'm doing it is a good idea, or if this "which spells you got?" test should be moved to OnSettingsChanged(), at the cost of not noticing your spell list has changed unless you twiddle something in the MCM. Or maybe there's a pre-existing function for detecting changes to the spell list. Or a way to run this check only every few minutes, or something. It just seems inefficient to ask what spells you have every single time. Might also be better to never return a value of "None" from that function checking the form ID lists. I assume I can feed it the default names of the spell to check as a second function parameter (which ultimately comes from the Property definition in the ESP, not in the script) as a default to fall back on. And probably also best to get the actual name of the spell for the error messages instead of saying "Candlelight" or "Magelight" in a hard-coded way – to account for translations and for mods that rename vanilla spells (the one I'm patching does so, and calls "Candlelight" by "Firefly I"). I THINK this will do all of that: Scriptname JaxonzLightsPlease extends Quest {Simple utility to improve Candlelight and Magelight casting} Bool bCandleLightOn = false GlobalVariable Property giCandleLightHotkey Auto GlobalVariable Property gfTimeScale Auto GlobalVariable Property giMageLightHotkey Auto Actor Property actPlayer Auto Spell Property splCandleLight Auto Spell Property splMageLight Auto Spell Property DefaultVanillaSpell Auto Sound Property sndFail Auto FormList Property CandleLightList Auto FormList Property MageLightList Auto Event OnInit() OnSettingsChanged() EndEvent Function OnSettingsChanged() ;update key mappings; called by MCM menu UnregisterForAllKeys() ;register keystroke callbacks RegisterForKey(giCandleLightHotkey.GetValueInt()) RegisterForKey(giMageLightHotkey.GetValueInt()) EndFunction Spell Function GetBestKnownSpell(FormList akSpellList, Spell DefaultVanillaSpell) ;assumes formlist is organized from least(0) to best(n) Int i = akSpellList.GetSize() Spell bestSpell = none While i > 0 && !bestSpell i-=1 Spell testSpell = akSpellList.GetAt(i) as Spell If testSpell If actPlayer.HasSpell(testSpell) Return testSpell EndIf EndIf EndWhile Return DefaultVanillaSpell EndFunction Function CastCandleLight() If (actPlayer.GetActorValue("Magicka") >= splCandleLight.GetEffectiveMagickaCost(actPlayer)) actPlayer.DamageActorValue("Magicka", splCandleLight.GetEffectiveMagickaCost(actPlayer)) splCandleLight.Cast(actPlayer) ; RegisterForSingleUpdateGameTime((splCandleLight.GetNthEffectDuration(0) as float) * 0.00028 * gfTimeScale.GetValue() ) ;convert spell duration seconds to game hours RegisterForSingleUpdate((splCandleLight.GetNthEffectDuration(0) - 1) as float) Else sndFail.Play(actPlayer as ObjectReference) ;insufficient magicka sound Debug.Notification("Insufficient magicka to renew "+splCandleLight.GetName()+", retry in 5 seconds...") RegisterForSingleUpdate(5.0) EndIf EndFunction Event OnKeyDown(Int KeyCode) if !Utility.IsInMenuMode() ;only pay attention to hotkeys in game mode ;see if there is an object grabbed If KeyCode == giCandleLightHotkey.GetValueInt() Spell testSpell = GetBestKnownSpell(CandleLightList, splCandleLight) If testSpell splCandleLight = testSpell EndIf If actPlayer.HasSpell(splCandleLight) bCandleLightOn = !bCandleLightOn If bCandleLightOn CastCandleLight() Else actPlayer.DispelSpell(splCandleLight) EndIf Else Debug.Notification("Unable to cast "+splCandleLight.GetName()+" because you don't know that spell") EndIf ElseIf KeyCode == giMageLightHotkey.GetValueInt() Spell testSpell = GetBestKnownSpell(MageLightList, splMageLight) If testSpell splMageLight = testSpell EndIf If actPlayer.HasSpell(splMageLight) If actPlayer.GetActorValue("Magicka") >= splMageLight.GetEffectiveMagickaCost(actPlayer) actPlayer.DamageActorValue("Magicka", splMageLight.GetEffectiveMagickaCost(actPlayer)) splMageLight.Cast(actPlayer) Else sndFail.Play(actPlayer as ObjectReference) ;insufficient magicka sound EndIf Else Debug.Notification("Unable to cast "+splMageLight.GetName()+" because you don't know that spell") EndIf EndIf EndIf EndEvent Event OnUpdate() ;Event OnUpdateGameTime() if bCandleLightOn CastCandleLight() Else ; UnregisterForUpdateGameTime() EndIf EndEvent It DID compile at least, but bears extensive testing. I did not move the FormIDList checks. It occurs to me that people can use spell deletion mods or the Console to remove spells at any time, so this really does need to be sure the spell is available. However, it bugs my sense of code efficiency that this is asking twice whether the player has the spell (once to decide which to use and again to decide to use it). I don't know enough about the language to be sure how to streamline that. I did put the functions at the top, though; just how I roll from too long using languages that require them pre-declared before use. Not sure the explicit declaration of DefaultVanillaSpell near the top is necessary if it's created as a function parameter name. Link to comment Share on other sites More sharing options...
foamyesque Posted April 11, 2019 Share Posted April 11, 2019 (edited) This doesn't compile, unfortunately. I first got:* (22, 24) Mismatched input 'int', expecting RPAREN* (22, 35) Required (...)+ loop did not match anything at input ')' After fixing "parent.OnKeyDown(Int KeyCode)" to read just "parent.OnKeyDown()", it got past that problem, but had other errors:* (22, 11) Argument keycode is not specified and has no default value* (36, 16) Cannot return a spell from getbestknownspell, the function does not return a value I tried integrating the gist of this into the original script (I wasn't actually quite sure what to do with a separate one): Those are both sloppy mistakes on my part; sorry. 'parent.OnKeyDown(int KeyCode)' should be 'parent.OnKeyDown(KeyCode)' (the parameter needs to be passed through), and 'Function GetBestKnownSpell(FormList akSpellList)' should be 'Spell Function GetBestKnownSpell(FormList akSpellList)' (I forgot to declare it as having a return value, so when I try to return one, the compiler chokes). Returning none as a 'this couldn't find any spell in the list on the player' is I think preferred. Any further checks can be done using 'is there anything in this variable', which is very very fast. Given you're re-writing the script, you could do this: Scriptname JaxonzLightsPlease extends Quest {Simple utility to improve Candlelight and Magelight casting} ;Bool bCandleLightOn = false GlobalVariable Property giCandleLightHotkey Auto GlobalVariable Property gfTimeScale Auto GlobalVariable Property giMageLightHotkey Auto Actor Property actPlayer Auto Spell Property splCandleLight Auto Spell Property splMageLight Auto Spell splLastCandleLight = none FormList Property CandleLightList Auto FormList Property MageLightList Auto Sound Property sndFail Auto Event OnInit() OnSettingsChanged() EndEvent Function OnSettingsChanged() ;update key mappings; called by MCM menu UnregisterForAllKeys() ;register keystroke callbacks RegisterForKey(giCandleLightHotkey.GetValueInt()) RegisterForKey(giMageLightHotkey.GetValueInt()) EndFunction Event OnKeyDown(Int KeyCode) if !Utility.IsInMenuMode() ;only pay attention to hotkeys in game mode if KeyCode == giCandleLightHotkey.GetValueInt() If KeyCode == giCandleLightHotkey.GetValueInt() splCandleLight = GetBestKnownSpell(CandleLightList) If splCandleLight if !splLastCandleLight if CastLightSpell(splCandleLight) splLastCandleLight = splCandleLight RegisterForSingleUpdate((splCandleLight.GetNthEffectDuration(0) - 1) as float) endif Else actPlayer.DispelSpell(splLastCandleLight) splLastCandleLight = none EndIf Else Debug.Notification("Unable to cast " + CandleLightList.GetAt(0).GetName() + " because you don't know that spell") EndIf ElseIf KeyCode == giMageLightHotkey.GetValueInt() splMageLight = GetBestKnownSpell(MageLightList) If splMageLight CastLightSpell(akLightSpell) Else Debug.Notification("Unable to cast " + MageLightList.GetAt(0).GetName() + " because you don't know that spell") EndIf EndIf EndIf EndEvent ;assumes formlist is organized from least(0) to best(n) Spell Function GetBestKnownSpell(FormList akSpellList) int i = akSpellList.GetSize() Spell bestSpell = none while i > 0 && !bestSpell i-=1 Spell testSpell = akSpellList.GetAt(i) as Spell if testSpell if actPlayer.HasSpell(testSpell) return testSpell endif endif endwhile return none EndFunction Event OnUpdate() ;Event OnUpdateGameTime() if bCandleLightOn if !CastLightSpell(splCandleLight) Debug.Notification("Insufficient magicka to renew " + splCandleLight.GetName() + ", retry in 5 seconds...") RegisterForSingleUpdate(5.0) endif Else ; UnregisterForUpdateGameTime() EndIf EndEvent bool Function CastLightSpell(Spell akLightSpell) If actPlayer.GetActorValue("Magicka") >= akLightSpell.GetEffectiveMagickaCost(actPlayer) actPlayer.DamageActorValue("Magicka", akLightSpell.GetEffectiveMagickaCost(actPlayer)) akLightSpell.Cast(actPlayer) return true Else sndFail.Play(actPlayer as ObjectReference) ;insufficient magicka sound return false EndIf EndFunction Function CastCandleLight() if (actPlayer.GetActorValue("Magicka") >= splCandleLight.GetEffectiveMagickaCost(actPlayer)) actPlayer.DamageActorValue("Magicka", splCandleLight.GetEffectiveMagickaCost(actPlayer)) splCandleLight.Cast(actPlayer) RegisterForSingleUpdateGameTime((splCandleLight.GetNthEffectDuration(0) as float) * 0.00028 * gfTimeScale.GetValue() ) ;convert spell duration seconds to game hours RegisterForSingleUpdate((splCandleLight.GetNthEffectDuration(0) - 1) as float) Else sndFail.Play(actPlayer as ObjectReference) ;insufficient magicka sound EndIf EndFunction I grouped up some of the common casting routines into a function named CastLightSpell() -- CastCandleLight is now redundant but left intact in case some external script calls it --, and patched a logic hole that arose when things got shifted to multiple spell variants (which I hadn't noticed beforehand). Magelight would have worked fine, but Candlelight is supposed to be sustained indefinitely, until the player turns it off. That means that the spells the player knows could potentially change during its lifetime, and if, say, the player learned a better candlelight, the dispel effect would've broken and left it permanently active. Now, when Candlelight is turned on, it saves the specific spell that was used and will dispel that, which should avoid the issue. This can also be used to track the casting state of Candlelight, rendering the boolean flag redundant, so I got rid of that. The code logic is slightly different than original in that my code will not start candlelight unless you can successfully cast the first five second's worth, whereas the original would toggle it on regardless and then try to renew. Edited April 11, 2019 by foamyesque Link to comment Share on other sites More sharing options...
Darklocq Posted April 12, 2019 Author Share Posted April 12, 2019 It DID work, after setting up properties in the ESP to pass in the FormID Lists (had to be done as Objects, though Arrays was more intuitive). Also did not work unless the active effect from Jaxon's script-spell that launches CandleLight was not on already in the loaded test save (had to go back to one that did not have it on already). However, it takes so long to go down the 5-spell checklist in the FormIDList that it's impractical (on my potato anyway), so I ended up using a globalvariable to store whether this check has been done, and then bypass the check if true (but force the check next time if you come up with no spell to use at all this time, or change the hotkeys). So, if you get new and better candlelight or magelight spells, you have to "register" them with the mod by either deleting your crappy ones or going into the MCM and changing something temporarily to trigger its "Function OnSettingsChanged()". This is functional enough for use now. Thanks for the help again. It was just exactly enough to get me on the path to working out enough of this language to not walk into the wall over and over again, metaphorically speaking. Here's the source as it stands now: Scriptname JaxonzLightsPlease extends Quest {Simple utility to improve Candlelight and Magelight casting} Bool bCandleLightOn = false GlobalVariable Property giCandleLightHotkey Auto GlobalVariable Property gfTimeScale Auto GlobalVariable Property giMageLightHotkey Auto GlobalVariable Property giJaxonzCandleLightSpellSet Auto GlobalVariable Property giJaxonzMageLightSpellSet Auto Actor Property actPlayer Auto Spell Property splCandleLight Auto Spell Property splMageLight Auto Spell Property DefaultVanillaSpell Auto Sound Property sndFail Auto FormList Property CandleLightList Auto FormList Property MageLightList Auto Event OnInit() OnSettingsChanged() EndEvent Function OnSettingsChanged() ;update key mappings; called by MCM menu UnregisterForAllKeys() ;register keystroke callbacks RegisterForKey(giCandleLightHotkey.GetValueInt()) RegisterForKey(giMageLightHotkey.GetValueInt()) giJaxonzCandleLightSpellSet.SetValueInt(0) giJaxonzMageLightSpellSet.SetValueInt(0) EndFunction Spell Function GetBestKnownSpell(FormList akSpellList, Spell DefaultVanillaSpell) ;assumes formlist is organized from least(0) to best(n) Int i = akSpellList.GetSize() Spell bestSpell = none While i > 0 && !bestSpell i-=1 Spell testSpell = akSpellList.GetAt(i) as Spell If testSpell If actPlayer.HasSpell(testSpell) Return testSpell EndIf EndIf EndWhile Return DefaultVanillaSpell EndFunction Function CastCandleLight() If (actPlayer.GetActorValue("Magicka") >= splCandleLight.GetEffectiveMagickaCost(actPlayer)) actPlayer.DamageActorValue("Magicka", splCandleLight.GetEffectiveMagickaCost(actPlayer)) splCandleLight.Cast(actPlayer) ; RegisterForSingleUpdateGameTime((splCandleLight.GetNthEffectDuration(0) as float) * 0.00028 * gfTimeScale.GetValue() ) ;convert spell duration seconds to game hours RegisterForSingleUpdate((splCandleLight.GetNthEffectDuration(0) - 1) as float) Else sndFail.Play(actPlayer as ObjectReference) ;insufficient magicka sound Debug.Notification("Insufficient magicka to renew "+splCandleLight.GetName()+", retry in 5 seconds...") RegisterForSingleUpdate(5.0) EndIf EndFunction Event OnKeyDown(Int KeyCode) if !Utility.IsInMenuMode() ;only pay attention to hotkeys in game mode ;see if there is an object grabbed If KeyCode == giCandleLightHotkey.GetValueInt() If giJaxonzCandleLightSpellSet.GetValueInt() == 0 Spell testSpell = GetBestKnownSpell(CandleLightList, splCandleLight) If testSpell splCandleLight = testSpell giJaxonzCandleLightSpellSet.SetValueInt(1) EndIf EndIf If actPlayer.HasSpell(splCandleLight) bCandleLightOn = !bCandleLightOn If bCandleLightOn CastCandleLight() Else actPlayer.DispelSpell(splCandleLight) EndIf Else giJaxonzCandleLightSpellSet.SetValueInt(0) Debug.Notification("Unable to cast "+splCandleLight.GetName()+" because you don't know that spell") EndIf ElseIf KeyCode == giMageLightHotkey.GetValueInt() If giJaxonzMageLightSpellSet.GetValueInt() == 0 Spell testSpell = GetBestKnownSpell(MageLightList, splMageLight) If testSpell splMageLight = testSpell giJaxonzMageLightSpellSet.SetValueInt(1) EndIf EndIf If actPlayer.HasSpell(splMageLight) If actPlayer.GetActorValue("Magicka") >= splMageLight.GetEffectiveMagickaCost(actPlayer) actPlayer.DamageActorValue("Magicka", splMageLight.GetEffectiveMagickaCost(actPlayer)) splMageLight.Cast(actPlayer) Else sndFail.Play(actPlayer as ObjectReference) ;insufficient magicka sound EndIf Else giJaxonzMageLightSpellSet.SetValueInt(0) Debug.Notification("Unable to cast "+splMageLight.GetName()+" because you don't know that spell") EndIf EndIf EndIf EndEvent Event OnUpdate() ;Event OnUpdateGameTime() if bCandleLightOn CastCandleLight() Else ; UnregisterForUpdateGameTime() EndIf EndEvent Link to comment Share on other sites More sharing options...
Darklocq Posted April 12, 2019 Author Share Posted April 12, 2019 (edited) Edit conflict: I didn't see your update before posting my own (I had this open in drafting mode for hours and hours while I was out of the house). I'll go over what you posted in your last version and see about integrating it, though the version I posted is functional so far (almost surprisingly, given I don't know what I'm doing in Papyrus). I'm not sure my way to do the globals I added is the most efficient. It's not clear how to do a global as a bool, but it seemed to work fine as Int. Anyway, I like most of what you did. I thought about keeping the list checker returning the default vanilla spell name on failure, so that the "you don't have a spell at all" message the player gets would report that name as what's missing ("Unable to cast Candlelight because you don't know that spell"). Then again, it might be fine to just say something like "Unable to cast a Candlelight-type spell because you don't know one", and thus be able to use this: "Any further checks can be done using 'is there anything in this variable', which is very very fast." While I didn't know of the technique at the time, "CandleLightList.GetAt(0).GetName()" is smarter than what I was doing, but unfortunately any accessing by FormID List values is noticeably, annoyingly slow on a potato (e.g., going through a list of 5 spells took 12 to 20 seconds on my box, depending on other conditions, like whether it was raining). So, I'm going to skip that kind of looking in the error message and try just the "Unable to cast a Candlelight-type spell because you don't know one" genericizing approach. I'm not sure the Candlelight would actually run forever if the spell went missing, since it's timed for 60 seconds or whatever it is. In my testing, I actually DID remove the spell while the effect was still running and I think it did eventually expire. Definitely, re-running the Candlelight caster, and having any alternative spell to use as found in the FormID list, did caused that to replace the current active (now missing) spell as the active effect. My "parent.OnKeyDown()" (without passing KeyCode explicitly) somehow appeared to work, at least for Candlelight, but was probably going to re-fire Candlelight if I tested Magelight. The current script doesn't use the parent thing now, so moot point. :-) I'm not sure what to do with the optional code about time scaling. One of your edits appears to have inadvertently turned one of those lines on, and another removes one from another section. It'll try to figure out what they do and re-integrate them for people who are using that feature of JLP (assuming it works at all – maybe Jaxonz commented it all out because it was faulty, for all I know). I don't want to use it myself, since i'm used to short-term magic effects working in realtime, and only long-term ones (like shrine blessings) working in game-time. I don't see any benefit to speeding up the rate at which this script fires! That would definitely happen to me sometimes, since I have a timescale mod that speeds up the clock in the wilderness, keeps it the same in the city, and reduces it indoors. Edited April 12, 2019 by Darklocq Link to comment Share on other sites More sharing options...
Darklocq Posted April 12, 2019 Author Share Posted April 12, 2019 (edited) Your version had compilation errors again. I worked around those, and integrated almost everything into a combined version. It has your merged function for firing off both spell types and your "what if the active Candlelight spell goes missing?" tests, and my global vars used to prevent the "run through the entire FormID list" stuff at every use, and so on. It compiles, and it seems to work at first, but it becomes "stuck". Once you press your Candlelight key, you get the spell working, but pressing the key again to turn it off does nothing, and it doesn't expire, but will just apparently run forever. Similarly, if you press the key for Magelight, it will fire one off, but then doing it again will not fire another one as expected. It doesn't seem to be failure of the event to close, since you can do Magelight then Candlelight or vice versa. Something's preventing it from running the same section of the event twice, though. Worse, it seems to result in CTD within a few minutes of use, which suggests some kind of runaway memory usage problem. Here's the code, with some temporary debug messages to show in-game where the script is at: Scriptname JaxonzLightsPlease extends Quest {Simple utility to improve Candlelight and Magelight casting} Bool bCandleLightOn = false GlobalVariable Property giCandleLightHotkey Auto GlobalVariable Property giMageLightHotkey Auto GlobalVariable Property gfTimeScale Auto GlobalVariable Property giJaxonzCandleLightSpellSet Auto GlobalVariable Property giJaxonzMageLightSpellSet Auto Actor Property actPlayer Auto Spell Property splCandleLight Auto Spell Property splMageLight Auto Spell splLastCandleLight = none FormList Property CandleLightList Auto FormList Property MageLightList Auto Sound Property sndFail Auto Function OnSettingsChanged() ;Update key mappings; called by MCM menu. This also triggers a re-read of the spell FormID lists at next use (to "register" new spells). UnregisterForAllKeys() ;register keystroke callbacks RegisterForKey(giCandleLightHotkey.GetValueInt()) RegisterForKey(giMageLightHotkey.GetValueInt()) ;set light spells as unknown to force the list to be re-parsed giJaxonzCandleLightSpellSet.SetValue(0 as Int) giJaxonzMageLightSpellSet.SetValue(0 as Int) EndFunction Spell Function GetBestKnownSpell(FormList akSpellList) ;assumes formlist is organized from least(0) to best(n) Int i = akSpellList.GetSize() Spell bestSpell = none While i > 0 && !bestSpell i-=1 Spell testSpell = akSpellList.GetAt(i) as Spell If testSpell If actPlayer.HasSpell(testSpell) Return testSpell EndIf EndIf EndWhile Return none EndFunction Bool Function CastLightSpell(Spell akLightSpell) Debug.Notification("akLightSpell passed = " + akLightSpell.GetName()) If actPlayer.GetActorValue("Magicka") >= akLightSpell.GetEffectiveMagickaCost(actPlayer) Debug.Notification("TEMP: There is sufficient Magicka.") actPlayer.DamageActorValue("Magicka", akLightSpell.GetEffectiveMagickaCost(actPlayer)) akLightSpell.Cast(actPlayer) Debug.Notification("TEMP: Spell should have fired.") Return true Else sndFail.Play(actPlayer as ObjectReference) ;insufficient magicka sound Debug.Notification("TEMP: There is NOT sufficient Magicka.") Return false EndIf EndFunction Event OnInit() OnSettingsChanged() EndEvent Event OnKeyDown(Int KeyCode) if !Utility.IsInMenuMode() ;only pay attention to hotkeys in game mode ;see if there is an object grabbed If KeyCode == giCandleLightHotkey.GetValueInt() If giJaxonzCandleLightSpellSet.GetValue() == 0 Debug.Notification("TEMP: giJaxonzCandleLightSpellSet = " + giJaxonzCandleLightSpellSet.GetValue()) splCandleLight = GetBestKnownSpell(CandleLightList) If splCandleLight giJaxonzCandleLightSpellSet.SetValue (1 as Int) Debug.Notification("TEMP: giJaxonzCandleLightSpellSet = " + giJaxonzCandleLightSpellSet.GetValue()) If !splLastCandleLight If CastLightSpell(splCandleLight) splLastCandleLight = splCandleLight RegisterForSingleUpdate((splCandleLight.GetNthEffectDuration(0) - 1) as float) Endif Else actPlayer.DispelSpell(splLastCandleLight) splLastCandleLight = None EndIf Else giJaxonzCandleLightSpellSet.SetValue (0 as Int) Debug.Notification("Unable to cast a Candlelight-type spell because you don't know one.") EndIf EndIf ElseIf KeyCode == giMageLightHotkey.GetValueInt() If giJaxonzMageLightSpellSet.GetValue() == 0 Debug.Notification("TEMP: giJaxonzMageLightSpellSet = " + giJaxonzMageLightSpellSet.GetValue()) splMageLight = GetBestKnownSpell(MageLightList) If splMageLight giJaxonzMageLightSpellSet.SetValue (1 as Int) Debug.Notification("TEMP: giJaxonzMageLightSpellSet = " + giJaxonzMageLightSpellSet.GetValue()) CastLightSpell(splMageLight) Else giJaxonzMageLightSpellSet.SetValue (0 as Int) Debug.Notification("TEMP: giJaxonzMageLightSpellSet = " + giJaxonzMageLightSpellSet.GetValue()) Debug.Notification("Unable to cast a Magelight-type spell because you don't know one.") EndIf EndIf EndIf EndIf EndEvent Event OnUpdate() ;Event OnUpdateGameTime() If bCandleLightOn If !CastLightSpell(splCandleLight) Debug.Notification("Insufficient magicka to renew " + splCandleLight.GetName() + ", retry in 5 seconds...") RegisterForSingleUpdate(5.0) EndIf Else ; UnregisterForUpdateGameTime() EndIf EndEvent Edited March 2, 2020 by Darklocq Link to comment Share on other sites More sharing options...
Recommended Posts