Skip to content

Commit

Permalink
races.as: Fixed and commented races.addCancelPoint()
Browse files Browse the repository at this point in the history
I've set out to thoroughly study the race system script by Neorej16. I would like to create an editor for it and make it multiplayer. Then I'd like to extend it with other game modes.

First thing I noticed is that there's an extensible callback mechanism - modder can call `races.setCallback()` with callback type and function satisfying `funcdef void RACE_EVENT_CALLBACK(dictionary@);`. When invoked, the function gets all params as dictionary. The available callback types are nicely documented directly in the raceManager setup:
```
		// we initialize the callbacks dictionary
		this.callbacks.set("RaceFinish", null); // when a race was finished
		this.callbacks.set("RaceCancel", null); // when a race was canceled by any means (race_abort box or forbidden user action)
		this.callbacks.set("RaceStart",  null); // when a race starts
		this.callbacks.set("AdvanceLap", null); // when a lap is done, but not when the race is done
		this.callbacks.set("Checkpoint", null); // when a checkpoint is taken (excluding start and finish)
		this.callbacks.set("NewBestLap", null); // When a new best lap time is set
		this.callbacks.set("NewBestRace", null);// When a new best race time is set
		this.callbacks.set("LockedRace", null); // When the user passes the start line of a locked race
		this.callbacks.set("RaceEvent", null); // When the user passes the start line of a locked race
		this.callbacks.set("PenaltyEvent", null); // When the user gets in a race_penalty box, handled by the raceEvent method
		this.callbacks.set("AbortEvent", null); // When the user gets in a race_abort box, handled by the raceEvent method
```

As you can see, there can be only one common callback of every type, and the entry in `this.callbacks` is "type name => function pointer". That's enough though, as each checkpoint has instance name of scheme "checkpoint|$raceID|$sequenceNumber|$splitTrailNumber" (yes, the race system supports tracks with multi-checkpoint split sections!) and this went as parameter to the callback every time.

However, there was one callback type other than the rest - cancel point callback. The entry in `this.callback` was "instance name => dictionary", where dictionary held all the info normally embedded in the instance name, and also field named "callback" with the actual function pointer. Apparently the programmer intended to support multiple cancel points, each with it's own callback. I don't understand why, as following the checkpoint mechanic would archieve the same, only instead of 2 functions you'd have one that decides what to do based on the arguments dictionary. Either way, there was a bug in the code: the `cancelPointCount` was never updated, so there could effectively be just one common callback, and it was still broken because as I already wrote it didn't encode info to instance name but kept it inside the `this.callbacks` dictionary.

While deciding how to fix it, I noticed there are in total 3 (!) callbacks for race cancelling - the broken cancel points, "AbortEvent" and "RaceCancel". Apparently "RaceCancel" was invoked each time race was aborted for any reason. "AbortEvent", though, would only be invoked when driving through an event box called "race_abort". I fulltext-searched our resources if we have any .ODEF with such eventbox, and no we don't. So either map creators knew, or it went unused. Either way, "AbortEvent" was basically the same thing as the broken cancel points, except that it kept data in instance names like checkpoints do. So I ended up fixing the cancel points by actually redirecting them to "AbortEvent", only ignoring the box name.

I'd hate to break Neorej16's code so I tested using modified Auriga Proving Grounds terrain (which uses one cancel point). I added a secondary cancel point and tested they're indeed broken as I thought. Then I tested again with my fix.
  • Loading branch information
ohlidalp committed Oct 15, 2023
1 parent 07ead5b commit 463e849
Showing 1 changed file with 39 additions and 58 deletions.
97 changes: 39 additions & 58 deletions resources/scripts/races.as
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/// \title Race and mission system
/// \brief Extension of original race scripting by Neorej16
/*
Hi,
Expand Down Expand Up @@ -32,6 +34,23 @@ raceBuilder::raceBuilderVersion)
-- neorej16
*/

/*
Following are dev notes from 2023's mission system project.
This involved thorough study of the existing logic, as original devs are no longer on the project.
Terminology:
- Instance name = unique label assigned to whole object in .TOBJ file or `game.spawnObject()` call.
- Box name = desired operation type specified as `event <boxname> [<filter>]` for each box in .ODEF file.
Instance name schemes = their meaning:
- "checkpoint|$RaceID|$ChpNumber|$SplitTrailNumber" = Start/Checkpoint/Finish. Use `races.addCheckpoint()` to create.
- "race_abort|$RaceID(-1 = any race)|$ChpNumber(??)" = Abort point (box name must be 'race_abort'). Cannot be added via `races` :(
- "race_cancel|$RaceID(-1 = any race)|$UniqueNum" = Abort point (box name doesn't matter). Use `races.addCancelPoint()` to create.
- "race_penalty|$RaceID(-1 = any race)|$ChpNumber(??) = Time penalization. Cannot be added via `races` :(
-- Petr Ohlidal
*/

// Define a function signature for the callback pointers
funcdef void RACE_EVENT_CALLBACK(dictionary@);

Expand Down Expand Up @@ -120,7 +139,7 @@ class racesManager {

// we initialize the callbacks dictionary
this.callbacks.set("RaceFinish", null); // when a race was finished
this.callbacks.set("RaceCancel", null); // when a race was canceled
this.callbacks.set("RaceCancel", null); // when a race was canceled by any means (race_abort box or forbidden user action)
this.callbacks.set("RaceStart", null); // when a race starts
this.callbacks.set("AdvanceLap", null); // when a lap is done, but not when the race is done
this.callbacks.set("Checkpoint", null); // when a checkpoint is taken (excluding start and finish)
Expand Down Expand Up @@ -151,7 +170,7 @@ class racesManager {
this.lastCheckpoint = -1;
this.raceStartTime = 0.0;
this.lapStartTime = 0.0;
this.cancelPointCount= 0;
this.cancelPointCount= 0;
this.lastCheckpointInstance = "";
this.lastRaceEventInstance = ""; // we only use this to boost the FPS
this.raceManagerVersion = "RoR_raceManager_v0.02";
Expand Down Expand Up @@ -943,69 +962,31 @@ class racesManager {
this.raceList[raceID].deleteCheckpoint(number, instance);
}

// if the user comes in this event box, the current race will be cancelled
// example usage: if the users drives off the track
// (race will be stopped)
void addCancelPoint(int raceID, const string &in objName, const vector3 &in pos, const vector3 &in rot, RACE_EVENT_CALLBACK @callback)
{
dictionary dict;
dict.set("raceID", raceID);
dict.set("oname", ""+objName);
// dict.set("position", vector3(pos));
// dict.set("rotation", vector3(rot));
dict.set("callback", @callback);
this.callbacks.set("race_cancel_"+cancelPointCount, dict);
game.spawnObject(objName, "race_cancel_"+cancelPointCount, pos, rot, "raceCancelPointHandler", false);
}
void addCancelPoint(int raceID, const string &in objName, const double[] &in v, RACE_EVENT_CALLBACK @callback)
// if the user comes in this event box, the current race will be cancelled.
// The `raceID` parameter can be -1 to abort any race or an existing raceID to abort only that.
// The optional callback parameter will be set as "AbortEvent" callback.
void addCancelPoint(int raceID, const string &in objName, const vector3 &in pos, const vector3 &in rot, RACE_EVENT_CALLBACK @callback = null)
{
addCancelPoint(raceID, objName, vector3(v[0], v[1], v[2]), vector3(v[3], v[4], v[5]), @callback);
if (@callback != null)
this.setCallback("AbortEvent", callback);

game.spawnObject(objName, "race_cancel|"+raceID+"|-1|"+cancelPointCount++, pos, rot, "raceCancelPointHandler", false);
}
void addCancelPoint(int raceID, const string &in objName, const vector3 &in pos, const vector3 &in rot)

// Convenience overload with pos+rot specified as double[]
void addCancelPoint(int raceID, const string &in objName, const double[] &in v, RACE_EVENT_CALLBACK @callback = null)
{
dictionary dict;
dict.set("raceID", raceID);
dict.set("oname", ""+objName);
// dict.set("position", vector3(pos));
// dict.set("rotation", vector3(rot));
dict.set("callback", null);
this.callbacks.set("race_cancel_"+cancelPointCount, dict);
game.spawnObject(objName, "race_cancel_"+cancelPointCount, pos, rot, "raceCancelPointHandler", false);
}
void addCancelPoint(int raceID, const string &in objName, const double[] &in v)
{
addCancelPoint(raceID, objName, vector3(v[0], v[1], v[2]), vector3(v[3], v[4], v[5]));
addCancelPoint(raceID, objName, vector3(v[0], v[1], v[2]), vector3(v[3], v[4], v[5]), @callback);
}

// Special callback, invoked from event boxes added via `races.addCancelPoint()`.
// It works exactly like a "race_abort" event box, except the box name (specified as `event` in .ODEF file) can be anything.
// This is required for backwards compatibility with older terrains.
// (For example: Auriga Proving Grounds uses event box named "checkpoint", see '31-trigger10x10.odef').
void raceCancelPointHandler(int trigger_type, const string &in inst, const string &in box, int nodeid)
{
if( this.state == this.STATE_NotInRace )
return;

dictionary dict;
if( not this.callbacks.get(inst, dict) )
return;

int raceID;
dict.get("raceID", raceID);
if( raceID != -1 and this.currentRace != raceID )
return;

this.cancelCurrentRace();

// call the callback function
RACE_EVENT_CALLBACK @handle;
if( dict.get("callback", @handle) and not (handle is null) )
{
dictionary args;
args.set("event", "cancel_point");
args.set("raceID", this.currentRace);
args.set("inst", ""+inst);
args.set("trigger_type", trigger_type);
args.set("box", ""+box);
args.set("nodeid", nodeid);
handle(args);
}
game.log("DBG raceManager.raceCancelPointHandler() called; trigger:"+trigger_type+", inst:"+inst+", box:"+box+", nodeid:"+nodeid);
this.raceEvent(trigger_type, inst, "race_abort", nodeid);
}

void recalcArrow()
Expand Down

0 comments on commit 463e849

Please sign in to comment.