Ted's RPG Rant

A place to rant about RPG games, particularly the Temple of Elemental Evil. Co8 members get a free cookie for stopping by. Thats ONE cookie each, no seconds.

Monday, April 24, 2006

Flag Tutorial (updated)

UPDATED 24/4/6

Note: Some changes have been made to how the OCF flags are handled! Re-read the end parts!

Winnings: $539

Woohoooo!!! The long awaited flags tutorial appears!

Well some at least... I will describe some of the basic ways of using flags, ones that come from inside the game and thus we know work. In some cases they are things I have discovered myself - be sure I will crow long and loud about those! In most cases they are things found by others such as Liv, Agetian and Dulcaion. Also, at some point we will look at individual variables (as opposed to the global kind) based on some stuff done by Agetian and Cerulean the Blue. Most importantly, this tutorial will be done in parts and will be upgraded at various points as I think of new stuff.

What are flags? Well, I am not talking the global.flags and global.vars, I am speaking about the various object flags, critter flags, npc flags etc that can be put into the prototype or mobbed into specific critters using ToEEWB. If you have used ToEEWB and opened a mob, or made a new one, you will doubtless have noticed long lists of flags. Lets look at them now:

Object Flags (OF_*)

OF_DESTROYED
OF_OFF
OF_FLAT
OF_TEXT
OF_SEE_THROUGH
OF_SHOOT_THROUGH
OF_TRANSLUCENT
OF_SHRUNK
OF_DONTDRAW
OF_INVISIBLE
OF_NO_BLOCK
OF_CLICK_THROUGH
OF_INVENTORY
OF_DYNAMIC
OF_PROVIDES_COVER
OF_RANDOM_SIZE
OF_NOHEIGHT
OF_WADING
OF_LOOTED
OF_STONED
OF_DONTLIGHT
OF_TEXT_FLOATER
OF_INVULNERABLE
OF_EXTINCT
OF_DISALLOW_WADING
OF_HEIGHT_SET
OF_ANIMATED_DEAD
OF_TELEPORTED
OF_RADIUS_SET

Item Flags (OIF_*)

OIF_IDENTIFIED
OIF_WONT_SELL
OIF_IS_MAGICAL
OIF_NO_PICKPOCKET
OIF_NO_DISPLAY
OIF_NO_DROP
OIF_NEEDS_SPELL
OIF_CAN_USE_BOX
OIF_NEEDS_TARGET
OIF_LIGHT_SMALL
OIF_LIGHT_MEDIUM
OIF_LIGHT_LARGE
OIF_LIGHT_XLARGE
OIF_PERSISTENT
OIF_MT_TRIGGERED
OIF_STOLEN
OIF_USE_IS_THROW
OIF_NO_DECAY
OIF_UBER
OIF_NO_NPC_PICKUP
OIF_NO_RANGED_USE
OIF_VALID_AI_ACTION
OIF_DRAW_WHEN_PARENTED
OIF_EXPIRES_AFTER_USE
OIF_NO_LOOT
OIF_USES_WAND_ANIM
OIF_NO_TRANSFER
OIF_FAMILIAR

Critter Flags (OCF_*)

OCF_IS_CONCEALED
OCF_MOVING_SILENTLY
OCF_EXPERIENCE_AWARDED
OCF_FLEEING
OCF_STUNNED
OCF_PARALYZED
OCF_BLINDED
OCF_HAS_ARCANE_ABILITY
OCF_SLEEPING
OCF_MUTE
OCF_SURRENDERED
OCF_MONSTER
OCF_SPELL_FLEE
OCF_ENCOUNTER
OCF_COMBAT_MODE_ACTIVE
OCF_LIGHT_SMALL
OCF_LIGHT_MEDIUM
OCF_LIGHT_LARGE
OCF_LIGHT_XLARGE
OCF_UNREVIVIFIABLE
OCF_UNRESSURRECTABLE
OCF_NO_FLEE
OCF_NON_LETHAL_COMBAT
OCF_MECHANICAL

Critter Flags 2 (OCF2_*)

OCF2_ITEM_STOLEN
OCF2_AUTO_ANIMATES
OCF2_USING_BOOMERANG
OCF2_FATIGUE_DRAINING
OCF2_SLOW_PARTY
OCF2_NO_DECAY
OCF2_NO_PICKPOCKET
OCF2_NO_BLOOD_SPLOTCHES
OCF2_NIGH_INVULNERABLE
OCF2_ELEMENTAL
OCF2_DARK_SIGHT
OCF2_NO_SLIP
OCF2_NO_DISINTEGRATE
OCF2_TARGET_LOCK
OCF2_ACTION*_PAUSED

NPC Flags (ONF_*)

ONF_EX_FOLLOWER
ONF_WAYPOINTS_DAY
ONF_WAYPOINTS_NIGHT
ONF_AI_WAIT_HERE
ONF_AI_SPREAD_OUT
ONF_JILTED -
ONF_LOGBOOK_IGNORES
ONF_KOS
ONF_USE_ALERTPOINTS
ONF_FORCED_FOLLOWER
ONF_KOS_OVERRIDE
ONF_WANDERS
ONF_WANDERS_IN_DARK
ONF_FENCE
ONF_FAMILIAR
ONF_CHECK_LEADER
ONF_CAST_HIGHEST
ONF_GENERATOR
ONF_GENERATED
ONF_GENERATOR_RATE1
ONF_GENERATOR_RATE2
ONF_GENERATOR_RATE3
ONF_DEMAINTAIN_SPELLS
ONF_BACKING_OFF
ONF_NO_ATTACK
ONF_BOSS_MONSTER
ONF_EXTRAPLANAR

This list is, of course, nicked from the forum HERE. Now, it is not the purpose of this blog to explain the meaning of these - thats what the thread is for - but rather to explain how to access them in-game. I will do this by demonstrating one of each type using an example from the game I know works. Lets start with a look at the most obvious:

OF_OFF

This is the most frequently used flag in scripts. Simply put, it turns the critter on or off: when off, the critter can still fire its scripts (eg heartbeat scripts) but will not be drawn or turn up on the radar for find_npc_near or anything like that.

To access the flag in a script, use the following commands:

________________attachee.object_flag_set(OF_OFF)

________________attachee.object_flag_unset(OF_OFF)

Simple, init? And so it goes for the various other Object Flags (OF_...). Or so I assume, no time to test them all! But note that a .mob set OF_OFF canNOT be unset by this flag! (Thanks Cerulean the Blue). I will keep an eye out for what can be over-ridden in the .mobs by scripts and what can't - I don't know if this is the only one or if that is a blanket thing. Annoying if it is - but I don't think so, because, (as you will see below) KOS_OVERRIDE in a script will speak over a .mobbed KOS flag (which is not the same as unsetting it... hmmm... wonder if the override exists precisely because you can't unset a .mobbed flag? More testing needed!)

Moving right along for the critter flags:

OCF_MUTE

This was sorta discovered by Liv and Dulcaion, I don't think it turns up in scripts in the original ToEE but is found a lot nowdays. It allows monsters to speak under very specific circumstances who would normally be mute (if they were not mute, they would make random comments if you snuck up and clicked on them, like NPCs without specific dlg files do. Having dire rats say, "good morning sir" when u click on them is less than necessary). So where the OCF_MUTE flag may be set by the prototype or the mob, it can be overriden and switched off in specific circumstances by the script, then reactivated when you are done talking to it. (The thread dealing with all this is HERE and is well worth reading for the issue at hand. It is also well worth reading for several other nostalgic reasons but a tutorial is no place to go into it. Note that I disagree with Dulcaion's assertion that unsetting a flag breaks the script - that sounds like the classic symptom of a minor cockup, I've had many. More testing needed).

So what do the scripts look like? Here they are, hot from the .py files:

def san_first_heartbeat( attachee, triggerer ):
________if (attachee.map != 5080 or game.global_flags[813] == 1):
________________attachee.critter_flag_set( OCF_MUTE )
________game.new_sid = 0
________return RUN_DEFAULT

Now... note that previoulsy I said it was flagS where the previous (object) and following (npc) flags are in the singular. I have now REVERSED that opinion, based on more extensive reading of Dulcaion's tests, and also some of my own experience. Thus that little script above has been altered from the one in-game, which is, again based on my experience, broken. Took me a while for it to sink in. (Thats a really perverse use of game.new_sid too, in context). That flag is meant to mute trolls found elsewhere than dungeon level 4 (where u can try to beat a password for the trap out of them. They don't actually know it - damn Liv was a champ! A red herring no less!) However, long-term readers of the forum will remember a funny pic I posted where I clicked on a troll and he said, "me no see you" because my guy was invisible. Now, my troll was on dungeon lvl THREE, in Thrommel's room (a random encounter caused by sleeping). It has always niggled at the back of my mind, why wasn't he mute when he wasn't on lvl 4? Leaving aside the issue of whether muting affects floating text (Liv pretty conclusively showed it does, Dulcaion disagreed) I think that shows the code I originally posted (saying attachee.critter_flagS_set( OCF_MUTE )) was probably broken. Also there is the common sense element that it should not be in a different form than the other flags (which require a singular flag thing) and finally, in Dulcaion's final post on this issue he made exactly this point. So barring conclusive testing otherwise, thats gonna be my official position.

Damn I hate it when I post something wrong in my tutorials! Hate it... please understand, I try to stick to things I have personally done or used, but periodically use scripts I have come across by Liv but not personally seen in action (like here), or in the game that I know werk. Let me at least assure you I NEVER just make stuff up and post it on the basis that it SHOULD work: amyone who has modded knows u hit problems with things that 'should' work all the time, thats why we need tutorials. Anyways, my apologies for the error.

So... npc flags :-)

ONF_KOS

Here's a common flag, and well worth being able to manipulate since changing critters to KOS (or not) depending on the player's actions makes for a nicely interactive game. Setting the flag in the prototype or mob is straightforward enough, but what about changes in game? One way is to use the san_will_kos thing in the protoype. That can look something like this:

def san_will_kos( attachee, triggerer ):
________if (game.global_flags[840] == 1 and attachee.map == 5065):
________________return SKIP_DEFAULT
________return RUN_DEFAULT

Simple enough, and speaks volumes about the difference between skipping and running the default scripts.

What about changing it on the fly from scripts? I had to discover this one myself, and what a pita it was!!! But it was damn useful since of course it will work for all NPC flags. Here is a snapshot of the upcoming Moathouse respawn:

________________bear1 = game.obj_create( 14052, location_from_axis (429L, 437L) )
________________bear1.rotation = 2
________________bear1.npc_flag_set( ONF_KOS )
________________bear2 = game.obj_create( 14053, location_from_axis (427L, 447L) )
________________bear2.rotation = 3
________________bear2.npc_flag_set( ONF_KOS )

I spawned some bears wandering around outside the walls of the moathouse, but since they were usually animal familiars, they had no KOS flags set. Can't have summoned animals attacking the party ;-)

Now... this brings us to the KOS_OVERRIDE script. I added one of these to KotB as I have a guy who runs amok under certain conditions, but unless u have met those conditions you should never get near him. So, I flagged him KOS with a thought to maybe altering it later. Well, later is fast approaching ;-) so I left him .mobbed as KOS and added the following script:

def san_first_heartbeat( attachee, triggerer ):
________if game.quests[4].state == qs_unknown:
________________attachee.npc_flag_set( ONF_KOS_OVERRIDE )
________return RUN_DEFAULT

Damn backwards ;-) should leave him normal then make him KOS when the time is right, but meh I say. It'll do for now - one reason I often do things backwards or in weird ways is to learn how to do new stuff. This worked for that end, so stop complaining!

Speaking of first_heartbeat files, here's a reminder of something I also put in the Well Whaddya Know? thread: The first time you enter a map, all the first_heartbeat files will fire everywhere. However, if you have been there before, they only fire as you enter their sector. So if you are adding to a first_heartbeat file, as well as using a save from off the map (I made that mistake with the file above, then wondered why it didn't work ;-)) you may have to enter the sector to get it to work, EVEN THOUGH if you have mobbed something new in it will work straight away whatever sector its in. That can be confusing if u r using little tells like having game.particles fire to let you know certain internal effects are working: when some work and others don't, you of course assume you have screwed up somewhere. Try walking into the sector first.

Ok, thats our basic NPC flags dealt with (OCF2 still to do). What about item flags? Mr Blue mentioned the other day that item flags look like this, as you might expect:

obj.item_flag_set( OIF_FLAG )

Again, its just common sense :-)

Soooooooo... what about reading flags? Here's something complicated by Dulcaion that will probably tell you more about reading flags than you want to know!

- First I collected a list of all the critters in the vicinity of my party (that includes the party members), calling it a

>>> a = game.obj_list_vicinity(game.party[0].location, OLC_CRITTERS)

- I then examined a and found out that the Murderous thief was item 5 (a[5]) with simply

>>> a

- I then examined OCF_MUTE to find out its binary value (it's 8192)

>>> OCF_MUTE

For reference, 8192 in binary is
0000 0000 0000 0010 0000 0000 0000

- Then I checked his critter flags

>>> a[5].critter_flags_get()

This returns 134488064, which is (in binary):
1000 0000 0100 0010 0000 0000 0000

So you can see that the mute flag IS set (the 0010 column lines up with a 1 between the two numbers in binary)

- Then I unset the mute flag with

>>> a[5].critter_flags_unset(OCF_MUTE) (Ted says: don't believe that flagS!!)

- I checked the results

>>> a[5].critter_flags_get()

this time, it was 134479872, or
1000 0000 0100 0000 0000 0000 0000

so you can see that the mute flag WAS cleared by the command.


I know its a pain me posting things and adding, "Btw I haven't tested it but its wrong" but again this was by Dulcaion and he basically added an errata saying as much himself. Anyways, I think that is well worth reading all that, because the flags themselves are of course stored as binary within the .mobs. This is why you can't click, say, the 'items' thing in ToEEWB on a .mob for an NPC: at the appropriate part of the .mob, clicking that will add

0000 0000 0000 0000 0000 0000 0000

to the .mob and kaboom, the damn thing doesn't work because NPC .mobs aren't meant to have item flags and the game can't read what it sees. It also explains no matter how many flags you set or unset, the .mob file doesn't change size, only adding a new TYPE of flag (or indeed all the flags of that type for the same size-price) will change the size. We don't all have to be Agetians, but every little bit of understanding helps. :-)

So, lets go back and see about the flag-reading thing. Here's another example from the upcoming moathouse spawn: and let me assure you this works, because I tested my a$$ off on it, complete with many private discussions with Mr Blue, and many different formats. Turned out I was doing the right thing all along - it was the fact my first_heartbeat files weren't firing on subsequent trips til I entered the sector that was making me think nothing was working. I sent C-Blue a screaming ranting (but also exulting) email about the evils of first_heartbeat files when I finally got to the end of it, and he hasn't spoken to me since ;-)

def san_first_heartbeat( attachee, triggerer ):
________y = (attachee.critter_flags_get() & OCF_MUTE)
________bandit1 = find_npc_near(attachee,14070)
________bugbear2 = find_npc_near(attachee,14172)
________gnolls1 = find_npc_near(attachee,14079)
________gnolls2 = find_npc_near(attachee,14066)
________if (attachee.map == 5005 and game.areas[4] == 1):
________________if game.global_vars[52] >= 2:
________________________game.obj_create( ??? )
________________________game.obj_create( ??? )
________________________game.obj_create( ??? )
________________________game.obj_create( ??? )
________________________game.global_vars[52] = 0
________________if y == 0:
________________________game.particles( "sp-summon monster I", game.party[1] )
________________________if (bugbear2 == OBJ_HANDLE_NULL):
________________________________game.obj_create( ??? )
________________________________game.obj_create( ??? )
________________________________game.obj_create( ??? )
________________________________game.obj_create( ??? )
________________________________game.obj_create( ??? )
________________________attachee.destroy()
________________else:
________________________game.particles( "sp-summon monster I", game.party[0] )
________________________if ((gnolls1 == OBJ_HANDLE_NULL) and (gnolls2 == OBJ_HANDLE_NULL) and game.global_flags[288] != 1):
________________________________game.obj_create( ??? )
________________________________game.obj_create( ??? )
________________________________game.obj_create( ??? )
________________________________game.obj_create( ??? )
________________________attachee.destroy()
________else:
________________return SKIP_DEFAULT
________return RUN_DEFAULT

Note the game.particles are merely there for testing purposes, I have left out what is spawned so there will be some surprises, and there is lots more to this (all the spawning stuff for the upstairs and outside). They were simple enough, make a .mob, stick it somewhere where it could tell if the players have been through or not (eg the outside one is in the middle of the bandits) then do a quick nearby NPC check to seee if the players went that way and wasted all the monsters. If so, spawn - if not, don't spawn. 'Game.area[4] == 1' is to check if the players have found the temple yet, thats the trigger for this new lot of content - if they have, then they have been gone long enough for the bodies to rot (dead monsters still show up on find_npc_near checks!) and they are a healthy level to come back and try this (ok, they may have just gone to Nulb, got the location from Otis, then raced back here, but if they do it that quick the bodies may not have rotted, so nothing will spawn - EVER - and they have ruined their added content. Ne'er mind - at that level they would not have survived these encounters anyways ;-))

So much for the upper levels (keep in mind there are not new critters in every room, just a few new encounters here and there to show what the new owner of the place is up to - namely, adding undead as she can). BUT for the lower levels, there are a number of encounters the players may never have done - if they use the secret stairs to go via the Zombies and Ghouls (skipping the much tougher Lugbash / Gnolls / Bugbears encounters) they could easily have gone straight through to Lareth, killed his men (one web and a couple charms and then sit back and enjoy :-)), dealt with him, gone out the back door, then come back later. Can't just add a bunch of new monsters to the gnolls' or bugbears' rooms under such circumstances, they will set on the gnolls and bugbears! (The gnolls have been respawned as skeletal gnolls, as you might expect - something rather more nasty has moved into the bugbears' old apartments). So, I need multiple .mobs running find_npc_near in different places (or, a counter added to their san_dying files, but if said bugbears and gnolls in the protos.tab turn up somewhere else, eg in random encounters or in new content added by people saying "hey those protos look ideal", this could be triggered wrongly.)

Multiple prototypes? Nah, too much effort ;-)

Multiple .mobs of one prototype? But then they will all be using the same first_heartbeat script. That means a lot of global flags, dunnit?

In the past, yes! But today, nah... u should know now how much I hate using global flags if I can help it. And as it turns out, I can help it here :-)

What I did was add OCF_MUTE to one of the mobs but not the other, and differentiated between them that way :-) Thats what the first variable 'y' does, it checks to see whether the attachee (that specific .mob) has the flag set, by logically ANDing it with the flags as read. If it does, its the .mob sitting in the middle of the gnolls (OF_OFF of course. game.global_flags[288], btw, is set by u bribing the gnolls: it would mean they would not show up as there, but of course they shouldn't have been lying around as corpses waiting to be reanimated as skeletal gnolls either, so in that case they don't show).

If it returns a zero on the AND, meaning the flag was not set, I know I am dealing with the bugbears' .mob, and it can test if they are around (I had a bugbear1 test as well but he would pick up one of the ones hiding in the little rooms behind the doors. People could easily forget them or not bother with them - I know I have done both in the past - so I am not going to care if they are there or not, as long as the rest have been dealt with, thats ok. If people want to pull the hiding bugbears into combat at their rear while dealing with the new monsters, thats their business).

Either way, they only fire once then are destroyed. The first to fire also deals with the issue of the main inhabitant (who is set up in Lareth's old rooms of course, not much of a spoiler that!), thats the first little spawny thing. game.global_vars[52] was put in the san_dying thing of Lareth's leiutenant and seargant, two unique critters who should not be showing up elsewhere, and as long as they are both dead this will fire (then it is reset to 0, only fires once). If you suggested one of them, then u r in trouble - but you shouldn't have suggestion at that level anyways!

Quick recap: to find if a critter flag is set, use:

attachee.critter_flags_get() & OCF_FLAG

Note that yes it IS flagS when getting but not when setting!!! That is tested and undisputed!

Next time, personal obj flags, they'll be thanks again to C-Blue! To go straight there, click HERE.

5 Comments:

At 2:29 am, Anonymous Anonymous said...

wut about OMG_LOL, WTF_KO n MF_PWNT flagz?

oh n its all gud yvy I voted 5 tiemz! hopz u win ^^

 
At 4:26 pm, Anonymous Anonymous said...

There's a lot of useful scripts there Ted, no dought I'll pliunder your blog for scripts as and when I ned to borrow them (and with the state of my scripting, lets hope I don't need to do so often).

 
At 5:30 pm, Blogger ShiningTed said...

They're copyright - leave your money on the fridge Al.

 
At 7:37 pm, Anonymous Anonymous said...

http://www.mistyeiz.com/2006/04/25/feeling-mushy-mushy/

*blush*

xoxoxo

 
At 10:12 am, Anonymous Anonymous said...

Yet more great codification of your knowledge. This needs backing up lest it be lost should your blog vanish mate :)

 

Post a Comment

<< Home