Nightmare skill detection

Today’s hack lets us only fire a trigger if the player is on nightmare skill. This allows you to alter the map compared to hard skill, when usually the only difference is the enemy ai. I thought for a change of pace I’d try to explain the thought process that goes into creating a hack like this, so we’ll be going slower than usual for a map hack entry.

We start with the knowledge that the skill setting is recorded in the QuakeC in a variable imaginatively named skill. So the first move is to find every bit of code in the standard source which uses skill. Running a full text search for “skill” gets us:

  • Some bits of code in boss.qc which detect easy skill
  • The definition in defs.qc
  • A bit in combat.qc: if (skill == 3) self.pain_finished = time + 5;
  • A false positive in the oldone.qc finale text
  • In subs.qc two sections
    • if (skill != 3) self.attack_finished = time + normal; in SUB_AttackFinished
    • if (skill != 3) return; in SUB_CheckRefire
  • if (skill == 3) CastLightning(); in shambler.qc
  • if (skill == 3) self.velocity = dir * 350; in shalrath.qc
  • The code that reads the engine skill setting in world.qc
  • The entity that changes skill settings in triggers.qc

So really there are five places that behaviour changes in nightmare skill. Even devoid of the context surrounding each line, I think one of them jumps out as being the most likely candidate – I wonder if you agree which one?

To me, it’s the line from shambler.qc, hands down, because it’s directly performing an action in nightmare that doesn’t happen elsewhere. It’s not just setting an obscure entity field we need carefully calibrated sensors to detect, it’s not making voreballs marginally faster, it’s firing an entire lightning bolt that isn’t usually fired. All we need to do is check the lightning bolt code to make sure it’ll fire from an info_notnull, and then rig up a shootable trigger for it to strike.

So here’s the whole of CastLightning():

void() CastLightning =
	local	vector	org, dir;
	self.effects = self.effects | EF_MUZZLEFLASH;

	ai_face ();

	org = self.origin + '0 0 40';

	dir = self.enemy.origin + '0 0 16' - org;
	dir = normalize (dir);

	traceline (org, self.origin + dir*600, TRUE, self);

	WriteEntity (MSG_BROADCAST, self);
	WriteCoord (MSG_BROADCAST, org_x);
	WriteCoord (MSG_BROADCAST, org_y);
	WriteCoord (MSG_BROADCAST, org_z);
	WriteCoord (MSG_BROADCAST, trace_endpos_x);
	WriteCoord (MSG_BROADCAST, trace_endpos_y);
	WriteCoord (MSG_BROADCAST, trace_endpos_z);

	LightningDamage (org, trace_endpos, self, 10);

OK, so we’re in the clear there, it’s certainly going to fire, but we might worry where it will go. The crucial part is that it fires in the direction of “self.enemy“. If we don’t set anything, then enemy will be the world entity. We shrug our shoulders now, the beam will fire towards the centre of the map, as it’s totally predictable there’s no need to do anything more fancy. Just rig the shootable trigger to stand between the info_notnull and the map’s origin.

The original inspiration for this question was to create a map where nightmare skill had different items and ammo to normal. In this scenario, it seems best to attempt to fire the trigger as soon as is possible, so we’ll do it on the spawn frame, by placing an entity with sham_magic11 (the name of the function with the shambler’s skill 3 variation) as its classname. Two important details are required for this implementation: 1) Make sure the shootable trigger is earlier in the entity list than this lightning entity, or the lightning will shoot at something that’s not there. 2) ensure either that both shootable target and lightning are later in the entity list than everything you’d like to remove (or put a short delay on the shootable target); otherwise you’ll try to remove entities before they are spawned.

So you’ve set a box up outside the map, with the sham_magic11 point entity inside, and a trigger_once with health positioned in-between it and the map origin. The map boots up, and the trigger_once fires, but almost instantly the map crashes with an error about a missing function? This is the next part of putting a hack together: a plan that looked perfect in theory has lots of little holes that you need to plug when put into practice. In our case, we thought we could just use sham_magic11, but in fact we’ve created a monster!

No, I’m not being melodramatic, we’ve literally made our little point entity genuinely believe that it’s a Shambler – but it really isn’t cut out to be one! Although we used sham_magic11 as a spawn function, it’s set sham_magic12 as our next think function as a side effect, and after 0.1 seconds that runs, just like the normal AI routine of a Shambler. While that think function doesn’t do any harm, it sets the NEXT think function to be sham_run1, and that’s the central point of a Shambler’s combat AI, so things unravel quickly.

sham_run1 calls ai_run, and the first thing every monster does is check whether their enemy has died yet. As we noted above, we haven’t set a proper enemy for this entity, just the world, which doesn’t have a health value. So the ai code judges the world to be dead, and tries to send this monster back to sleep by calling the ai_stand function. But because we’re not a real monster at all, we don’t have an ai_stand function set, so a crash ensues.

Luckily, this is not only easy to fix, but we can take advantage of the opportunity to clean up the entity. Set the th_stand key on the lightning entity to the value SUB_Remove. Once that portion of the ai code is reached, the code doesn’t crash, because we set a function to run. That function also deletes the entity which calls it, so there’s no further ai to worry about, and we even got the entity slot back to use later. Mission accomplished? I’d say so.


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )


Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.