CompMod Design

characterext.py

Acts as a single include point for other modules. Since the methods are attached to the character class, there is no need to include each individual module.

Includes modelutil, idutil, disposition, expression and then adds a few of its own methods:

TraceLine() - Find a point infront of or behind the PC/NPC

TraceCircle() - Same as traceline, but allows degree offset such that 90 would be a point to the PC's right.

GetItems() - Retrieve a list of the PC/NPC's items.

GetWeapon() - Return string of weapon that the user is currently using.

GetKeys() - Return list of keys that the user has.

IsHuman() - Is the NPC human? This function cross references statutil.py. If NPC is not found there, it

uses a global lookup table that bases the decision on the value of vclan.

IsStealth() - Is Obfuscate activated or the NPC squating.

GetLevel() - Examines all attributes\skills to determin a level value between 0 and 20.

Near() - Given a point and a radius, returns true or false if the entity is close to that point (within 3D)

GetData() - See Below

SetData() - See Below

CloneData() - Used to clone the G.npcdata info from another NPC instance.

CloneDataFromID() - Used to clone the G.npcdata info from another NPC's key value.

IsClan() - This function cross references statutil.py. If NPC is not found there, it

uses a global lookup table that bases the decision on the value of vclan.

The most important methods are GetData and SetData. These methods manage G.npcdata which is a hashmap of hashmaps and is pivitol to this mod. Normally attribtes created on runtime instances are lost when the user quits the game. IE:

npc.current_location=2

This creates an attribute on the npc object called "current_location" with an initial value of 2. However, when the user quites and reloads, the value is lost. By using the new SetData and GetData commands, the attributes could persist.

npc.SetData("current_location",2)

current_location=npc.GetData("current_location")

Module relies on idutil's GetID() method to generate unique keys for the G,npcdata hashmap of hashmaps.

C. Utilities

Utilities are pure python modules which help with non-VTMB specific issues such as file-IO and logging. Ironically I put "util" at the end of most of my module names, however this was a convention I started early on before I hammer out the design... by then it was too late.

┌─ [ logutil.py ]

[ fileutil.py ] ──│

│

└─ [ configutil.py ]

│

│

[ vUserDict.py ]

fileutil.py

This file contains a selection of basic file IO methods. One thing that makes the methods special is that they are all path aware and refuse to operate if the path does not contain "Vampire". This was done as a sanity check. For the most part, file-IO is limited to reading in files and directories. The one exception is consoleutil.py which must write the console commands that we wish to execute to a local temporary file before sending a command to the game to execute the contents of the file.

fileutil is a completely stand alone library that does not actually need VTMB to compile.

vUserDict.py

A python 2.1.2 library that I imported to allow the creation of an extended hashmap class that could self load. The version of Python included with VTMB is old and did not support class extension, so this module was downloaded and included as a work around.

vUserDict is a completely stand alone library that does not actually need VTMB to compile.

configutil.py

Defines the Options class : An extended hashmap class that can read in key value pairs from a commented file and expose them via a new "Options" class. The options class offers a get(key,defaultvalue) function to ensure if the config file does not exist or there are permissions issues reading the file in that a suitable default value is available.

configutil relies only on vUserDict and fileutil. This module does not need VTMB to compile.

logutil.py

Defines a log function called "log" which takes a string and a log level. Uses configutil's options class to detect log level. By centralizing logging, I can send data to file or standard out (console).

logutil relies only on configutil. This module does not need VTMB to compile.

eventutil.py

expressionutil.py

dispositionutil.py

statutil.py

Several modules within this mod deal with HashMaps. Hashmaps provide pretty fast lookup, but you have to have a unique key. The unique key for NPCs and PCs became a portion of the model file path. I define the logic here instead of simply parsing the .model property throughout the code so that I can easily upgrade the hash and hashing function at a later time if performance or memory consumption become an issue. Unfortunately changing what GetID() returns for an NPC's unique key would break any and all previous save games.

The following methods are added to the character class:

GetID() - Returns game unique ID string.

** GetUniqueID() - Returns game unique ID string.

IsPC() - Returns true if model appears to belong to a PC

IsEmbraced() - Returns true if model appears to be embraces (ends with _e.mdl).

** Experimental. Not used in version 1.0

modelutil.py

Defines a number of utility functions for scanning an NPC's model directory for new models. Also maintains the hashmap G.default_models. The persistent hashmap is needed because original models are normally hidden in vpk files and not present on the filesystem. So it is stored in a map for restoration when a user cycles through all the visible models\skins available for an NPC.

The following methods are added to the character class:

GetModels() - names of mdl files in npc's model directory

GetSkins() - abbreviated names of models (hence the term skin)

SetSkin() - set skin using an abbreviated name

SetSkinByIndex() - set skin using index. 0 = first skin in list returnd by GetSkins ()

NextSkin() - Sets skin to the next skin available. Cycles to beginning if at last skin.

PrevSkin() - Goes the opposite direction of nextskin.

HasMoreSkins() - indicates if NPC has more than 1 skin.

Module relies on idutil's GetID() method to generate unique keys for the G.default_models hashmap.

idutil.py

Requirements

Initial requirements driven by other modern RPG titles:

Baldurs Gate:

- Provided large selection of potential companions so that balanced party could be achieved regardless of class choices.

- Allowed Possession of companions

- One of the first games to include concept of class specific havens

- Allowed inventory management of Companions

- Provided Companion specific dialogs and quests

- All companions either traveled with you or reported to companion specific location.

Knights of the old Republic:

- Provided 9 static companions. Choice was not in companions but in which 2 would travel with you.

- Allowed Possession of companions

- Included concept of haven where you would store unused companions.

- Allowed inventory management of Companions

- Provided Companion specific dialogs and quests

- Only 2 companions could travel with you. But fights were smaller to compensate.

Companion Mod:

- Combining aspects of BG and KOTR, large pool of companions, but only 7 total at any time.

- Needed to allow Possession of companions.

- VTMB already has clan specific havens. (4 different haven possibilities). Just need to update so that companions could be stored there.

- Needed to allow inventory management of Companions - Needed to edit dialogs to allow Companion specific dialogs and quests

- Like Kotr, Only allow two companions to travel with you.

Main Design

Requirement I : Traveling Companions

A) How to get companions that follow PC from map to map?

Issue 1:

All map objects and entities only exist within the map they are defined in. VTMB does not automatically transfer objects between maps. So an NPC can be told to follow you within a map but as soon as you change maps, they will disappear because they do not exist on the new map and the game engine does not support moving objects between maps. Testing showed that the PC inventory, stats and global variables were the only things maintained by the engine when you move between maps.

Design:

Three variables were created to track companion information

G.companions: A list of companions in your party.

G.henchmen: A list of companions traveling with you.

G.npcdata: Table of tables maintaining CompMod specific information regarding each companion listed in G.companions.

To create the illusion of a companion following you from map to map, I needed to pre-embed companions on every map in the game. Originally, I was going to embed 2 hidden NPC's and change their model based on who was following you. However, combat bugs (discussed later) revealed that I needed to be able to re-spawn the NPC. So instead, I decided to install npc_maker entites on every map. . These entities are capable of spawning an NPC hundreds of times over if need be.

To detect map traversal, I used a VTMB entity called "logic_auto". When embedded into a map, this entity fires a python function every time the player enters the map from another map. My design involved embedding these entities into every map as well. They fired a function not only identifying map traversal but also identifying the map name the PC had just entered.

Issue 2:

The maps contain narrow corridors, steps and ladders that a normal NPC follower can not traverse.

Design:

I embed a logic_timer entity on every map that fires a polling event every 15 seconds. When the poll fires, it checks to see if any of the companions are too far away from the PC. If they are, they are teleported behind the PC.

Related Modules:

companion.py : Provides functions for managing G.companions and G.henchmen variables as well as telling the spawned NPC to follow you. (Basically the heart of the mod). Also handles events thrown by NPCs created with the npc_maker classes.

characterext.py: Provides GetData() and SetData() methods used to access and manage G.npcdata. Also provides Near() method used to decide if NPC is too far away.

vamputil.py: Acts as an event relay. Most events fire a global method in Vamputil which relays the event to specific modules that need notification. In this case the events include the OnMapEnter and OnPoll.

B) Companion Combat

Issue 1:

Original game didn't have travel companions so combat AI sucks. By default, friendly companions attack all enemies (even if the enemies haven't seen you). Also, the engine doesn't limit the NPC's sight by walls. It uses a simple radius check. So not only do they blow cover, they attack enemies you can't even see yet.

Design:

Companions are spawned deaf, blind and stupid. At least these are the settings that the npc_maker uses. When combat begins, we change the npc's vision and hearing so that they can see the enemies and thus begin fighting. When Combat ends, we make them blind, deaf and dumb again.

To detect the beginning and end of combat, I embedded the entity "events_world" on every map and hooked the OnCombatMusicStart and OnNormalMusicStart events up to some methods in vamputil.py which in turn activate methods within companion.py to signify the beginning and end of combat. Later, the AI was improved so that when combat began, not only did I allow the Companion to see, but I also activate disciplines on the Companion if they qualify.

Issue 2:

When the PC goes stealth, the companion can still be seen.

Design:

I detect when the PC goes stealth or uses obfuscate. When this is detected, I apply obfuscate to the NPC, thus making them invisible to enemies.

To detect stealth, I embed a logic_timer entity on every map that fires a polling event every 15 seconds. The polling events compares the PC's origin with their center. If the pc's origin is close to their center, they are squatting and thus playing stealth. While not very Zen, the poll provides a work around for many things that can't normally be detected.

Issue 3:

Once an NPC engages someone in combat, making them blind does not stop the combat.

Design:

Through testing, I found there was no way to end a grudge once a fight had started. The only solution was to re-spawn the NPC (once again blind, deaf and dumb). The new NPC clone didn't hold any grudges.

Re-spawning the npc required the use of npc_maker entites embedded onto each map.

Requirement 2 : Possession

Todo...

Modularized Components

The CompMod provides three primary capabilities. While there are many supporting modules, each capability centers around a central module.

A. Primary Modules

1) companion.py

Contains methods for adding, removing and replacing your traveling and non-traveling companions:

addToParty(npc, type=1): Creates a temporary NPC and starts a conversation with them. Wether you can actually add them or not depends on if there is room.

removeFromParty(): Removes henchmen from party. Their name is added to G.rcomplist. As you enter every map, the game looks to see if any of the NPCs in the list are present and unhides them if they are.

removeHenchman(): Tells a travelling companion to return to the haven.

replaceHenchman(name): When you must ask a henchmen to return to the haven to make room for someone new, this method swaps the new NPC into the old henchmens slot.

All companion interaction is done through dialog. So there are a number of methods defined to help determine when dialog messages should appear and to support companion functionality:

FeedTest(pc,npc): Can we feed on the current companion?

Feed(npc): Like it says

ShowFeedTimer(npc): How long till you can feed again?

makeRanged(npc): Sets combat mode to ranged/melee weapons

makeMelee(npc): Sets combat mode to melee only.

makeFollowPC(npc): Tell companion to follow you

stopFollowPC(npc): Tell companion to stio following you.

hasMaxHenchmen(): returns true if you already have 2 henchmen

hasMaxCompanions(): returns true if you already have 7 companions.

openInventory(): Every map has a container embeded on it. You can access the container by using this command on a henchman. Event handlers execute when you open and close the container, synchronizing the contents with the variable G._inventory[].

embrace(npc): Only works on henchmen. Executes embrace animation and updates companion data in G.npcdata. Note that vampire models must be set up for this to work or it will break.

Last but not least, companion.py is where all the event handlers are defined for your traveling companions. Note the word "travelling". If they are in your haven, then their events redirect to methods in havenutil.py

Events fire for situations such as Combat Beginning. The companion spotting a new entity and even dialog ending with one of the companions. Event handlers are the key to your companions AI, allowing them to use their dicsiplines in combat or teleport behind you if they get too far away.

There are a few interdependencies with the primary module, possessutil.py. The companions OnDialogEnd event is handled by companion.py, but you can activate possess or unpossess which requires calling a method in possessutil.py. So there are minor dependencies as a result.

2) possessutil.py

Contains methods for possessing and unpossessing your traveling companions. It also handles possession oriented events. The two most noteworthy are auto-unpossess when you begin dialog and auto-unpossess during combat.

The first event is supported by edits to every dialog in the game. A little bit of code was added to the starting condition block of all the dialog files to fire a global OnDialogBegin event, which redirects to possessutil.OnBeginDialog, which determines if you can talk to the NPC using the current body. The decision is based on a set of lookup tables which define when conversation is allowed and not allowed.

The second event is created by event_player entities embedded into every map which call the global event OnPlayerDamaged which relays the event to possessutil.OnPlayerDamaged() to determine if we need to force unpossess.

There are a large number of helper methods that do everything from deterining if certain dialog options should be available to calculating how much XP to re-imburse the PC when they unpossess.

3) havenutil.py

Provides methods to support non-travel companions located in haven. Focuses primarily on the Pose system, but includes enough haven centric methods and event handlers that I decided to call it havenutil.py.

One must understand that most of the poses require embedded scripted_sequence entities that are only present in haven maps.

Includes support methods for spawning and clearing companions when you enter the haven.

B. Support Modules

Originally I was going to create several support classes that included lots of miscellaneous helper functions. Then I realized that most if not all of the functions required an NPC or a PC parameter. So instead, I made a set of utility classes that extend the base Character Class with new methods. Instead of passing the NPC into a global function, you call a function on an NPC instance. This is a much nicer design, though it did increase the number of Character class methods considerably.