Search the Community
Showing results for tags 'nvse4'.
-
Req knowledge: UDFs, blocktypes, forms and refs, advanced use: arrays Also required: NVSE 4.6+ 1. Why? 2. Standard events, handling them, parameters. 3. Using the standard parameters as filters 4. Removing event handlers 5. NVSE events 6. User-defined events 1. Why? As a modder there are certain things that you know aren’t impossible, but you’d rather not do them because it causes conflicts and other problems. How would you go about: altering a ‘personal hygiene’ variable every time someone activates a sink, tub or toilet? playing a woozy double vision image space modifier every time the player consumes booze? playing a wolf whistle sound every time an actor unequips their clothing? playing the theme from The Good, the Bad and the Ugly every time you equip a revolver? setting a chance of a booby trap materializing and going off if certain locked doors are opened by the player? monitoring changes in an actor’s AI packages, so that you can force them to stick to the one they’re supposed to follow? marking each item that is dropped from player inventory in a particular cell for deletion, to avoid savegame bloat, or to mimic things getting picked up because you’re not around to watch over them? having people say custom dialog whenever they hit other people with a particular weapon or weapon type? The only way I can see so far of doing any of those is attaching an object script to an item, with a one-frame blocktype like OnActivate, On(Actor)Equip, OnOpen, OnPackageChange, OnDrop or OnHit(With) signaling that these events happen so you can do something about it. The problem there is you can’t just go around attaching object scripts to vanilla forms without inviting conflicts with other mods changing anything about those forms, and creating a need for separate patch mods resolving those conflicts. FNV already has a significantly reduced maximum number of mods that can be loaded compared to other Beth games, and if a large portion of those are simple patches, that’s just a waste. Not to mention you have to make those patches, and keep repeating to people reporting problems to install them, in the right order. Another issue is that you’ll usually need to attach the same script to a lot of objects if you want it to work correctly. This means all the base forms for sinks, toilets & tubs for your hygiene mod, or all the revolvers if you wanted to have people say "Yippee-ki-yay, motherfucker" if they hit others with revolvers. And what about mod-added items that could just as easily apply? Would you like to make a patch for each and every revolver mod out there? Not me. A lot of times your script will also fire when it’s not needed: you want your item cleanup script to only work on some actors, or only want to have a chance of booby traps on some door refs while other doors sharing the same base form should be safe. The only way of detecting if the necessary conditions apply is by already running the script, and already inviting the conflicts by simply having it on the form. These headaches are enough to make you want to skip adding some cool functionality altogether sometimes. What if... - we could be notified of anything being activated, opened, dropped, hit, consumed etc without attaching scripts to vanilla forms? - we could filter being notified according to what is being activated or who’s doing the activating, what is being opened or who’s doing the opening, etc? That’d be sweet, huh? It’s exactly what event handlers do. 2. Standard events, handling them, parameters. Event handlers are UDFs that you register with NVSE to be called whenever their associated event happens. These handlers are then set for the rest of the game session; even if you load a different save, they are still valid until you actually restart the game. If I want to be notified whenever anything whatsoever is activated or dropped, for instance, I register an event handler with SetEventHandler every time the game is loaded: SetEventHandler "OnActivate" myActivateEventHandler SetEventHandler "OnDrop" myDropEventHandler and then every time something is about to be activated or dropped, NVSE will call your event handler before proceeding with the activation or dropping. Remember that: certain functions that would work in a regular block will not in their equivalent event handler, because what's about to happen hasn't yet. Using the GetActionRef function in an "OnActivate" handler will probably not return anything - luckily you'll be provided with the same intel via the handler's parameters. In the case of the “OnDrop” event, NVSE will pass along the dropper as first parameter, and the dropped item as second. So your myDropEventHandler needs to be able to capture them, in that order: Scn myDropEventHandler ref rDropper ref rDropped Begin Function {rDropper, rDropped} … End If you don’t have those variables ready to capture the parameters, nvse will report an error. In the case of the “OnActivate” event, you need to remember that the terminology with the OnActivate script block and the activate function in vanilla can be confusing. Light switches, sinks and triggers are found under “activators” in the geck although they always seem to be activated by an actor. While it looks like an actor activates something like a container ref, when you force it via script it’s actually the container ref doing the activating: rContainerRef.activate rActorRef The “OnActivate” event, then, first expects the activator, the thing that looks to be activated, and then the thing or actor that seems to activate it, which you’d be able to retrieve from a regular OnActivate script block with GetActionRef. So let’s put it this way: Scn myActivateEventHandler Ref rActivator ref rActionRef Begin Function {rActivator, rActionRef} … End Most of the standard event handlers, the ones that correspond with regular blocktypes, come with standard parameters like that. They will always be sent, be ready to receive them. (The params don’t have to be called that, of course, the names are just meant to distinguish what they are.) 3. Using the standard parameters as filters These handler parameters can be used as filters, by adding them as parameters to the SetEventHandler function when you register the handler. This makes it so that your event handler is only called in when it’s needed, and you should obviously use such filters as much as possible. Also make sure that whatever they filter on is actually a valid form, otherwise the filtering will bug out and it'll be as if no filter was applied. Because you may choose to only use one filter, or none at all, you need to specifically state on which parameter you’re filtering, the first or the second, and couple them with double colons (like you do with ar_Map): SetEventHandler "OnDrop" myEventHandler "first"::playerref "second"::myCoolgunForm In OBSE, the “first” was called “ref”, and the “second” was called “object”, and this is still possible in NVSE: SetEventHandler "OnDrop" myEventHandler "ref"::playerref "object"::myCoolgunForm However, unlike OBSE, NVSE allows either of those filters to refer to a ref, base form, or even a formlist (!) containing refs and/or base forms and/or more formlists, when applicable. So using “first” and “second” is much less confusing in that regard. So, give or take some scripting in the actual event handler scripts, the problems from the introduction should be a lot less problematic with the following events and filters set: 1. SetEventHandler "OnActivate" myEventHandler "first"::formlistofallsinkforms "second"::playerref 2. SetEventHandler "OnMagicEffectHit" myEventhandler "first"::playerref "second"::ChemIncCHAlcohol 3. SetEventHandler "OnActorUnequip" myEventhandler ; check what’s unequipped with the handler’s parameter 4. SetEventHandler "OnActorEquip" myEventhandler ; check what’s equipped with the handler’s parameter 5. SetEventHandler "OnOpen" myEventHandler "first"::formlistofdoorrefs "second"::playerref 6. SetEventHandler "OnPackageChange" myEventHandler "first"::rActor "second"::rPackage 7. SetEventHandler "OnDrop" myEventHandler "first"::playerref ; check the cell and the dropped item in the handler 8. SetEventHandler "OnHitWith" myEventHandler "second"::myCoolGunorListofGuns For the package monitoring problem, it’s also entirely possible to monitor packages starting, changing and ending in one and the same handler: SetEventHandler "OnPackageStart" myEventHandler "first"::rActor "second"::rPackage SetEventHandler "OnPackageChange" myEventHandler "first"::rActor "second"::rPackage SetEventHandler "OnPackageDone" myEventHandler "first"::rActor "second"::rPackage In this case you can use the GetCurrentEventName function to figure out what event has called your handler: Scn myEventHandler Ref rActor ref rPackage string_var sv_event Begin Function {rActor, rPackage} Let sv_event := GetCurrentEventName if eval sv_event == "OnPackageStart" ; do something elseif eval sv_event == "OnPackageChange" ; do something else endif End 4. Removing event handlers It’s entirely possible that you provide certain settings in your mod that cause an event to no longer apply. Also, event handlers are still valid if you re-load a different save but not re-start the game, so you want to build in some safety by removing a handler if, for instance, that different save hasn't gone through some init yet that the handling needs. To remove a handler you use RemoveEventHandler, with the same syntax as the one you used when you set it: RemoveEventHandler "OnActivate" myEventHandler "first"::rActivator "second"::rActionRef The above will remove the event handler being called when those filters apply. If those are the only ones you set in the first place, you might as well skip the filters: RemoveEventHandler "OnActivate" myEventHandler So yes, you can set and remove an event handler being called under some filters and not others. 5. NVSE events Aside from events related to vanilla blocktypes, you can also register handlers to be called when certain meta-game events happen. This is especially of use for mods that write some type of co-save files etc. (After testing, it seems this is for NVSE plugins only.) 6. User-defined events As if all of that wasn’t cool enough, you can fire off custom events that other people can register to in the same way as they would a standard one, and receive information that you send along with it. This is more naturally of use to modders working on some type of general functionality, wishing to let other modders know that something happened that is of note and non-continuous, something that can classify as an ‘event’. Perhaps you’ve overhauled sneaking and wish to advertise that according to your sneaking mod, the player’s been detected or has successfully hid, whether or not vanilla functions agree. Perhaps your mod needs to go through a series of init scripts before it’s functional and you wish to let dependent mods know that yours is ready for action when it’s done. Perhaps you want your inventory sorter to not only notify others that sorting took place, but also send along information on where everything ended up. Perhaps even you’ve been slaving over a naughty mod that monitors naughty things happening until one of the naughty people involved encounters some type of climactic event and you wish other naughty mods to know this so they can do cool stuff like play appropriate sounds and what not. In each of those cases, all you really need to do is dispatch an event with the DispatchEvent function, which should at least be followed by an event name, optionally a stringmap with intel, and optionally another string overriding the default sender name: DispatchEvent "eventName":string args:stringmap sender:string DispatchEvent "SomeEventName" Array_var ar_mystringmap ; populate ar_mystringmap DispatchEvent "SomeEventName", ar_mystringmap DispatchEvent "SomeEventName", ar_mystringmap, "somestring" And the mods wishing to register a handler to it will do it as normal, with the following limitations: - because there are no standard parameters to use, you can’t use filters when setting handlers for custom events - the only parameter that the handler UDF needs and has to have is an array var that can capture the stringmap that’s always received, even if none was dispatched: SetEventHandler "SomeEventName" myEventHandler Scn MyEventHandler Array_var args Begin Function {args} … End If no stringmap was dispatched, it’ll only contain the 2 standard elements: [eventName] == "SomeEventName" [eventSender] == "SomeSenderString" (usually == "DispatchingMod.esp") The dispatcher can determine “SomeSenderString” by explicitly adding the third optional parameter in the DispatchEvent line. If it’s left blank it’ll be the dispatching mod’s name. If a stringmap was dispatched, the standard elements are added to it. That’s it. Go forward and modify.
-
This is the first installment of a set of guides that’ll explain and advertize some of the fancy things that come with NVSE4 and are new to the FNV modding community: UDFs, string vars and array vars. Oblivion modders unfamiliar with the more advanced functionalities of OBSE (16+) may also get some use out of it. These guides are aimed at people who understand an average vanilla script – give or take looking up some functions – but feel a little lost looking at the OBSE documents when they try to figure out how to use all that on a practical level. Let’s start with the basics: some brand new syntax that does more with less. Have a look at this list of OBSE expressions. If scripting is a language, let’s call the functions the vocab and idioms, and what you’re seeing there the syntax, the stuff you need to put it all together and make sense of it. Some of those should already be familiar to you: == != > < >= <= + - * / () && || Comparison, some basic math, parentheses overriding regular precedence, the logical operators AND and OR – you got that, right? If not, put this aside, come back when you do. Let’s skip to the most useful of the new syntax features, by which I mean the ones we'll definitely need to get the other parts to work, as well as some that are just too handy to ignore: 1. LET 2. IF EVAL 3. "MATH" AND ASSIGN 4. WHILE LOOPS 5. SCRIPT COMPILER OVERRIDE 1. LET In nvse4, “let” takes over from good old “set”. Sure, you can still use “set” for whatever you used it before, but “let” can do more, and it’s good practice to switch to using that whenever you work with the more advanced syntax below, and string and array vars, which we’ll discuss in another installment. Basically, all you need to remember is that instead of Set someVar to someValue you go let someVar := someValue See that := thingie there? Don't forget it. It means "assign", ie "let A := B" means "let A be B" - in a biblical "let there be light" kinda way. 2. IF EVAL What goes for set-let goes for if-if eval and the elseif counterparts. “If eval” will be needed to have the compiler understand some of the more advanced stuff. Use it exclusively with anything involving array and string variables, and it won’t let you down. If eval SomeCondition Elseif eval SomeCondition Else endif Note that you can mix old ifs with new if evals in the same if-block, if you want. if eval someCondition elseif someCondition endif 3. "MATH" AND ASSIGN += add and assign -= subtract and assign *= multiply and assign /= divide and assign Indispensible, if you ask me, for quickly manipulating any float; it pretty much performs the math function on the var/value to left with the var/value to the right and immediately sets the var/value on the left to the result. It may not look like much, but goddamn, we can finally be done with typing out lines like Set someQuestID.someVar to ThatSameQuestID.ThatSameVAR + 1 and just switch to let someQuestID.someVar += 1 Less typing, shorter scripts, no reason to stick to the old way. Oh, and how about those timers: Set fTimer to fTimer + GetSecondsPassed If fTimer > 10 ; do something Endif can just be If eval (fTimer += GetSecondsPassed) > 10 ; do something Endif Use it, love it, never look back. 4. WHILE LOOPS While loops pretty much combine the looping aspect of the Label/GoTo loops we’ve had so far, with the possibility of making the loop dependent on an inherent “if eval” condition (including that ‘math and assign’ stuff above). A while loop is anything between a "while" condition and a "loop" command; it’s a block like an if-endif block: While somecondition ; check/do stuff Loop If the while-condition is false, what’s in that block will be skipped. If the while-condition is true, the block between it and the loop command will execute, and on hitting “loop”, the script will return to the while-condition to evaluate it again. Let's get some examples in: Want some chunk of code to run 80 times? Used to be you had to do: Label 1 If iNum < 80 Set iNum to iNum + 1 ; run your code GoTo 1 Endif The while equivalent is: While (iNum += 1) < 81 ; run your code loop Again, it's shorter, and it has the added advantage that nvse doesn't have to bother to keep track of label indexes, and neither do you for that matter. Additionally, the loop is represented as a block, whereas the GoTo is hardly ever on the same level of indentation as your label, and the endif to your if was rarely ever in play. That may sound a little finnicky, but the more complicated your script, the more readability is a factor. Label/GoTo is ugly.Do note however that iNum is already added to on the first time the while-condition evaluates, so it starts at "1" rather than at "0" in the if-condition of the old example, so for an exact 80 loops, I raised the value to 81. Let’s apply this to your classic formlist walk. With label/goto, your average backwards loop, doing stuff with each element in the list & then erasing it from your formlist, would look like: Set iCount to ListGetCount rList Label 1 If 0 < iCount Set iCount to iCount – 1 Set rForm to ListGetNthForm rList iCount ; do stuff with rForm ListRemoveNthForm rList iCount GoTo 1 Endif With while: Let iCount := listgetcount rList While 0 <= (iCount -= 1) ; note: iCount is already subtracted from rightaway, so I made it a <= Let rForm := ListGetNthForm rList iCount ; do stuff with rForm ListRemoveNthForm rList iCount Loop And of course, just erasing without doing stuff with the list's elements can be as simple as while (listgetcount rList) ListRemoveNthForm rList 0 loop While loops are obviously pretty sweet and a lot more elegant than Label/GoTo, but like any other loop function you should take care that it does have a chance to end and doesn't go into an infinite loop that's bound to crash your game. while (iNum += 1) > 0 ; do stuff loop is BAD: instant CTD.Also, stop and consider what you're doing before you run a very large, intensive chunk of code in a while loop that runs a lot of times. The while loop is still supposed to run within the frame that the script it's in runs, after all. Definitely limit your while loop body to things that actually depend on the while condition being different. You can stop a while loop with the "break" command, which will move the script to the line below "loop" - this is obviously something you should do whenever you can to avoid code being run that shouldn't or doesn't have to be: let iNum := listgetcount ActorsInCellList while (iNum -= 1) >= 0 let rActor := ListGetNthForm ActorsInCellList iNum if rActor.IsChild let iChildAlert := 1 break endif loop if iChildAlert == 0 ; do stuff you don't want kids to be around endif You can skip a part of the while loop's body with the "continue" command, which, like a 'return', will immediately move the script back to the while-condition: let iNum := listgetcount ActorsInCellList while (iNum -= 1) >= 0 let rActor := ListGetNthForm ActorsInCellList iNum if rActor.GetIsReference playerref continue endif ; do stuff you won't want to apply to the player loop As always, it's good practice to have those "continues" as high up in your loop's body as you can, same as a return in a spell or object script. 5. SCRIPT COMPILER OVERRIDE Note: you may need to know a bit more about string vars, array vars and user-defined functions to really understand some of what’s below. But the compiler override definitely falls under syntax and isn’t limited to any of those subjects, so there. Sure, let, if eval, while, math & assign etc. are pretty sweet in themselves already, and we haven't hit the spot of looking at combinations of UDFs, string vars, array vars and nx vars yet. Still, when we get to that, we're gonna be hampered sometimes by the vanilla script compiler itself, which doesn't accept those as parameters to vanilla and old nvse functions. Example #1: let's say we have a number that we want to get the floor of, the nearest whole number lower than the float. The vanilla compiler will know what we want to do if we specify it as a float variable that we pass to the floor function as a parameter: let iSomeInt := floor fSomeFloat It will not understand what we want though if we tell it to get the float from an array element or from a UDF call: let iSomeInt := floor somearray[someIndex] let iSomeInt := floor call someUDF will not compile. The vanilla compiler can only accept float variables as parameters to the floor function, and has no idea what's waiting in that array or that UDF at compile time anyway. Could be anything, and nuh-uh, it ain't standing for it.To get around that, you have to switch over the script block to use the NVSE compiler instead, which you do by prepending the block name with an underscore: Begin _Gamemode (_Menumode, _Scripteffectstart, _OnActivate, _Function, etc etc) Let someInt := floor somearray[somekey] Let someInt := floor (call someFunction) End and then you can go about your business, getting that floor, you badass. You’re basically telling the compiler to take it on faith that it’s gonna be fine, you know that there’s a float waiting under that key in that array, or that a float is bound to come back from that UDF. You’re taking over some of the responsibility from the compiler, in exchange for more freedom. In turn, you’ll need to keep your head, not fuck up, test stuff in-game instead of relying on error reports in the geck, and use parentheses a little more to make sure the compiler doesn’t neatly compile something that can’t possibly work in-game. Example #2: let’s say we want to check a bunch of different SPECIAL actor values on an actor and give them a bump if they’re under 5, then usually that’d mean: ref rActor Begin OnActivate if rActor.GetAV Charisma < 5 rActor.SetAV Charisma 5 endif if rActor.GetAV Intelligence < 5 rActor.SetAV Intelligence 5 endif ...yawn... I'm not gonna bother with the other ones. Huge waste of script space, bound to give you RSI End Now, you should know that the parameters Charisma, Intelligence etc are each in fact a string, a combination of characters. To shorten the chore of checking and setting 7 actor values, we can stick them in an array, retrieve them in a foreach loop and stick them in a string var, and just check them like that: ref rActor array_var entry array_var somearray string_var somestringvar Begin _OnActivate let somearray := ar_list "Agility", "Charisma", "Endurance", "Intelligence", "Luck", "Perception", "Strength" foreach entry <- somearray let somestringvar := entry[value] if eval 5 > rActor.GetAV somestringvar rActor.SetAV somestringvar 5 endif loop End but, well, that's just a roundabout way of showing the override will allow a string_var rather than the actual string for the actor value. Strictly speaking we can shorten that even more: ref rActor array_var somearray array_var entry Begin _OnActivate let somearray := ar_list "Agility", "Charisma", "Endurance", "Intelligence", "Luck", "Perception", "Strength" foreach entry <- somearray if eval 5 > rActor.GetAV entry[value] rActor.SetAV entry[value] 5 endif loop End Example # 3 : just the example given in the OBSE docs, which if you understood the previous ones, you should be able to get now: string_var axis float pos array_var somearray Begin GameMode let axis := somearray[index] ; the axis paramater for the setpos function is held in an array as a string that can be "x", "y" or "z" let pos := (call someFunction) + someRef.GetPos z ; the setpos function will only accept a float for the position parameter so that needs to be calculated first if eval axis == "z" setpos z pos elseif eval axis == "y" setpos y pos elseif eval axis == "x" setpos x pos endif End becomes the epically short begin _gamemode setPos someArray[index], (call SomeFunction) + someRef.getPos z end ---------------- An added advantage of using the script compiler override is that instead of Set fHealth to rActor.GetAV Health ; hey, hey or: let fHealth := rActor.GetAV somestringvar/somearrayelement, remember? you can refer to the actor value by its code number, which you can find at the bottom of the woefully outdated nvse (read: fose) docs: let fHealth := rActor.GetAV 16 Why is this an advantage? Well, you can do stuff with numbers that you can’t with strings like “health”. Like sticking them in an int: Let fHealth := rActor.GetAV iSomeInt So that you could pretty much loop through a bunch of actor values with a while and math and assign loop too, for instance: int iSomeInt Begin _ScriptEffectStart Let iSomeInt := 24 While (iSomeInt += 1) < 32 ; loops through 25-31, the AV codes for your body parts’ condition if eval 100 > GetAV iSomeInt SetAV iSomeInt 100 endif loop End in case you want to make some "Doc's Super Duper Doctor's Bag" or "Instant Heal Button" something
- 42 replies