Roaming Monsters - FreeSlave/halflife-updated GitHub Wiki

Introduction

Sometimes a mod author wants a monster to roam in the area, simulating some action, instead of just standing at the spot. Sven Co-op introduced a concept of roaming monsters. Monsters with freeroam parameter set to Always will freely roam between graph nodes on the map. There's also a global parameter for worldspawn to make all monsters freeroam by default.

In this tutorial we'll implement this feature in Half-Life SDK.

Implementation

All changes that we're going to apply reside in the server code, in the dlls directory.

First, go to the schedule.h and add a new task type somewhere in the list of task types.

+TASK_GET_PATH_TO_FREEROAM_NODE,
LAST_COMMON_TASK,		// LEAVE THIS AT THE BOTTOM!! (sjb)

Now we need to implement this task. Go to schedule.cpp, in function CBaseMonster::StartTask add the code at the end of the switch (but before the default label!)

case TASK_GET_PATH_TO_FREEROAM_NODE:
{
	if( !WorldGraph.m_fGraphPresent || !WorldGraph.m_fGraphPointersSet )
	{
		TaskFail();
	}
	else
	{
		for( int i = 0; i < WorldGraph.m_cNodes; i++ )
		{
			int nodeNumber = ( i + WorldGraph.m_iLastActiveIdleSearch ) % WorldGraph.m_cNodes;

			CNode &node = WorldGraph.Node( nodeNumber );

			// Don't go to the node if already is close enough
			if ((node.m_vecOrigin - pev->origin).Length() < 16.0f)
				continue;

			TraceResult tr;
			UTIL_TraceLine( pev->origin + pev->view_ofs, node.m_vecOrigin + pev->view_ofs, dont_ignore_monsters, ENT( pev ), &tr );

			if (tr.flFraction == 1.0f && MoveToLocation( ACT_WALK, 2, node.m_vecOrigin ))
			{
				TaskComplete();
				WorldGraph.m_iLastActiveIdleSearch = nodeNumber + 1;
				break;
			}
		}
		if (!TaskIsComplete())
		{
			TaskFail();
		}
	}
}
	break;

Monsters perform tasks through the schedules, so we should define one. Go to schedule.h again, and add a new schedule type to the list of schedule tasks.

+SCHED_FREEROAM,
LAST_COMMON_SCHEDULE			// Leave this at the bottom

The schedule type is declared. Now let's add a schedule definition in the defaultai.cpp (before the CBaseMonster::m_scheduleList):

Task_t tlFreeroam[] =
{
	{ TASK_STOP_MOVING, (float)0 },
	{ TASK_WAIT_RANDOM, 0.5f },
	{ TASK_GET_PATH_TO_FREEROAM_NODE, (float)0 },
	{ TASK_WALK_PATH, (float)0 },
	{ TASK_WAIT_FOR_MOVEMENT, (float)0 },
	{ TASK_SET_ACTIVITY, (float)ACT_IDLE },
	{ TASK_WAIT, 0.5f },
	{ TASK_WAIT_RANDOM, 0.5f },
	{ TASK_WAIT_PVS, (float)0 },
};

Schedule_t slFreeroam[] =
{
	{
		tlFreeroam,
		ARRAYSIZE( tlFreeroam ),
		bits_COND_NEW_ENEMY |
		bits_COND_LIGHT_DAMAGE |
		bits_COND_HEAVY_DAMAGE |
		bits_COND_HEAR_SOUND,
		bits_SOUND_DANGER,
		"Free Roaming"
	},
};

Here we defined a list of tasks the schedule will use, including the new TASK_GET_PATH_TO_FREEROAM_NODE we added earlier. It has some TASK_WAIT and TASK_WAIT_RANDOM tasks in it, so monster will wait a bit before proceeding to the next node. You can change these parameters as you want. We also defined the schedule itself, referencing the list of tasks and setting the condititions that can interrupt the schedule (seeing an enemy, getting damage or hearing a danger sound, e.g. grenade before detonation).

You can also add slFreeroam to the CBaseMonster::m_scheduleList, but it's not necessary.

There's still no connection between SCHED_FREEROAM schedule type and slFreeroam schedule definition. Go to the CBaseMonster::GetScheduleOfType function and add the code at the end of the big switch (but before the default label!):

case SCHED_FREEROAM:
	{
		return slFreeroam;
	}

So far, so good, but nothing works yet :)

Remember we were talking that freeroam feature can be set globally on the map in Sven Co-op? Let's make some modifications in the worldspawn entity before we actually make monsters to use this feature.

First, go to the world.cpp and a new flag for world:

#define SF_WORLD_FORCETEAM	0x0004		// Force teams
+#define SF_WORLD_FREEROAM	0x0008		// Monsters freeroaming by default

If the flag 0x0008 is already used in your mod, choose another value (must be a power of 2, e.g. 0x0010).

Go to the CWorld::KeyValue function and add this code (before the call to CBaseEntity::KeyValue):

else if ( FStrEq( pkvd->szKeyName, "freeroam" ) )
{
	if (atoi( pkvd->szValue ))
	{
		pev->spawnflags |= SF_WORLD_FREEROAM;
	}
	return true;
}

Note that halflife-updated changed the KeyValue signature and thus it uses return true. In original Half-Life SDK it would be pkvd->fHandled = TRUE;

Then, in cbase.h add a new variable to CWorld class:

static bool gFreeRoaming;

Return to world.cpp, and at the start, after the #include directives, add:

bool CWorld::gFreeRoaming = false;

At the end of CWorld::Precache function add:

if ((pev->spawnflags & SF_WORLD_FREEROAM) != 0)
{
	gFreeRoaming = true;
}
else
{
	gFreeRoaming = false;
}

Now, we can finally move to the monsters code. Define these constants somewhere in basemonster.h:

#define FREEROAM_MAPDEFAULT 0
#define FREEROAM_NEVER 1
#define FREEROAM_ALWAYS 2

Add a function to the CBaseMonster class declaration:

Schedule_t* GetFreeroamSchedule();

Also add a variable to the CBaseMonster class declaration:

short m_freeRoam;

We've done with header. Let's go to the implementation, to the monsters.cpp.

Add a new entry in the CBaseMonster::m_SaveData:

DEFINE_FIELD( CBaseMonster, m_freeRoam, FIELD_SHORT ),

In the CBaseMonster::KeyValue add:

else if ( FStrEq( pkvd->szKeyName, "freeroam" ) )
{
	m_freeRoam = (short)atoi( pkvd->szValue );
	return true;
}

Again, the return true part is specific to halflife-updated. Write pkvd->fHandled = TRUE; in original Half-Life SDK.

Go to the schedule.cpp and add GetFreeroamSchedule implementation (actually you can do it in monsters.cpp, but all schedule related code is in schedule.cpp, to let's stick to the tradition):

Schedule_t* CBaseMonster::GetFreeroamSchedule()
{
	if (m_freeRoam == FREEROAM_ALWAYS)
		return GetScheduleOfType( SCHED_FREEROAM );
	else if (m_freeRoam == FREEROAM_MAPDEFAULT)
	{
		if (CWorld::gFreeRoaming) {
			return  GetScheduleOfType( SCHED_FREEROAM );
		}
	}
	return NULL;
}

We've done with preparations and now we can make modifications to the monsters' behavior. Go to the CBaseMonster::GetSchedule method and make some additions under the MONSTERSTATE_IDLE case:

else if (FRouteClear())
{
	// no valid route!
+	Schedule_t* freeroamSchedule = GetFreeroamSchedule();
+	if (freeroamSchedule)
+		return freeroamSchedule;
	return GetScheduleOfType(SCHED_IDLE_STAND);
}

This way when monster has nothing to do in Idle state, the monster will try to roam on nodes. There's however another state which monster enters by some conditions, e.g. after the fight ends. So make additions under the MONSTERSTATE_ALERT case too:

+Schedule_t* freeroamSchedule = GetFreeroamSchedule();
+if (freeroamSchedule)
+	return freeroamSchedule;

else if (HasConditions(bits_COND_HEAR_SOUND))
{
	return GetScheduleOfType(SCHED_ALERT_FACE);
}
else
{
	return GetScheduleOfType(SCHED_ALERT_STAND);
}

Patch

FGD preparation

Before we proceed to testing, let's make some changes to the fgd of your mod.

Add a new parameter for the worldspawn entity:

freeroam(choices) : "Roaming monsters (node graph)" =
[
	0 : "No"
	1 : "Yes"
]

Add a new parameter for the Monster BaseClass.

freeroam(Choices) : "Monster Roaming (nodes)" =
[
	0 : "Map Default"
	1 : "Never"
	2 : "Always"
]

These definitions are from svencoop.fgd and we implemented the feature to be compatible with Sven Co-op.

Testing

Make a map, place some nodes (info_node) and some monsters (for testing purposes they should be prisoners or ally to the player, or you put them behind the glass wall to observe). First, let's test the global free roaming. Open worldspawn properties (called Map properties in some editors) and set Roaming monsters to Yes. Compile the map and run in the game. Monsters will walk go between nodes.

Then, let's set some monsters to never roam. Open some monster's properties and set Monster Roaming to Never. This time all monsters except for those you specifically set not to roam will walk between nodes.

Open Map properties again and set Roaming monsters to No. Set some monsters' Monster Roaming to Always. Only these monsters will roam now.