Follower system¶
Maps can entirely control the follower systems which is the system that dictates which followers is allowed to follow the player. It also manages the presence of chompy
as a follower.
The system can be configured on the map with the following configuration fields:
Name | Type | Description | Default |
---|---|---|---|
canfollowID | int[] | The animIDs that are allowed to follow the player party on this map when present in instance.extrafollowers . This restriction is ignored if the GameObject's name is 0 (which is normally only the case for the TestRoom map) |
Empty array |
followerylimit | float | The maximum following distance (absolutely value) in y allowed for any entity following another. If the y distance between the entity and its followee gets higher than this value and we aren't in a minipause , the DoFollow of the entity will teleport them to their followee instantly. This should NEVER be negative because the distance is meant to be expressed as an absolute value |
20.0 |
closemove | bool | This field if true allows the map to force the CloseMove entity follow logic of the player party. NOTE: The influence of this fiels is extremely complex and inconsistent, primarily during the BanditHideout stealth section, more details can be found in the section below |
false |
An explanation of the follower system¶
There's an important distinction between the player requesting an entity to follow the party and the MapControl allowing said followers. They are actually managed at 2 different places:
- instance.
extrafollowers
tells the animIDs that wants to follow the player party. This array is saved on the save file - instance.
map
.canfollowID
tells which animIDs are allowed to follow the party. Since it's a configuration field, it is meant to have its value come from prefab, but AreaSpecific can do modifications on it if needed
This distinction is important because it means that even if instance.extrafollowers
contains an animid and even if this information is loaded from the save file, the current map has the final say on allowing or denying the following. If the animid isn't in its canfollowID
, the entity won't appear and no following will happen.
The only exception to this rule is when the GameObject's name of the MapControl is 0
. Since this name always corresponds to the map id, it means it should only apply to the TestRoom
map. On that map specifically, the canfollowID
rule doesn't apply and any animid in instance.extrafollowers
will be added as a follower.
The followers that are actually present are stored in the map's tempfollowers
. This array excludes the main player party member, but it does include chompy
or any other followers.
MainManager.AddFollower¶
As for how a follower is added, it's through the MainManager.AddFollower method:
public static EntityControl AddFollower(EntityControl caller, int id)
There are 2 ways to use this method:
- Adding an existing entity as follower: Doing this involves recreating the entity by first destroying it before creating a new one at the same position and the new one will become the follower. To do this,
caller
should be not null and correspond to the entity to add as follower in this fashion. In this mode of operation,id
's value won't be used and it will be overriden to thecaller
's animid - Adding a follower from a completely new entity: Doing this simply involves creating the entity and adding it as follower. To do this,
caller
needs to be null andid
needs to contain a valid animid
Here's the procedure this method does to add the follower and store it into the map's tempfollowers
:
- If caller isn't null (meaning this entity needs to be destroyed before recreating it):
- caller is unfixed
- The id parameter is overriden to the caller's
animid
and it is added to instance.extrafollowers
- The caller is destroyed
- If id is 0 or above:
- The last entity in the follow chain is obtained. It's the
player
if it's notfollowedby
anyone otherwise it's the lastplayerdata
.entity if that one isn'tfollowedby
anyone otherwise it's the lastmap
.tempfollowers
- CreateNewEntity is called to create an entity with id as the animid,
Follower X
as the name whereX
is the id and the follow being the last entity in the follow chain obtained earlier. The position depends on if this is replacing an entity or not:- If it's replacing an entity, it's the position of that entity before it was destroyed
- If it's not replacing an entity, the position is the
player
position + instance.globalcamdir
.forward.normalized * the length ofplayerdata
* 0.05 (this prevents z fighting by offsetting the follower slightly towards the camera)
- If
map
exists (it's not being loaded), the newly created entity is childed to themap
(otherwise, the entity remains rooted) - The entity gets a
PFollower
tag and itstempfollower
field is set to true which marks it as a player follower. This registers them to be implicated in the EntityControl follow system - The
player
'snpc
list is reset to a new list - The newly created entity is added to the
map
'stempfollowers
- The last entity in the follow chain has its
followedby
set to the newly created entity which completes the link of the new follower in the chain
- The last entity in the follow chain is obtained. It's the
As for the return value, it's not used by the game, but the method returns the entity whose followedby
was set to the new follower. It's possible the method returns null, but it's only under an erroneous condition and that is that the value of id (after it's overriden if applicable) is negative which means it's not a valid animid. This isn't supposed to happen because normally, either caller
is specified and its animid
isn't negative or caller
is null, but id has a non negative value. It's still possible that neither case are true and in which case, null will be returned, but it indicates that an invalid follower was attempted to be added.
MapControl's flow for adding followers¶
While AddFollower can be called manually well after the map is loaded, MapControl can call it in its first LateUpdate (known as latestart
). This is the part that enforces the canfollowID
whitelist, but also manages the presence or absence of chompy
For adding any followers other than chompy
, here's how this process goes:
- As mentioned earlier, if the map is
TestRoom
, AddFollower is called for each instance.extrafollowers
as the id and null as the caller so every follower the player has is allowed - Otherwise, only the ones present in
canfollowID
will be added as followers in the same way. After AddFollower is called, the following is done:- The follower's
tempfollowerid
is set to the matchingcanfollowID
index. NOTE: This is incorrect, see the section below for details on thetempfollowerid
issues - If the follower's animid is
Maki
, itsccol
gets a height of 3.0 and a center of (0.0, 1.5, 0.0) - The follower's
onground
is set to false (this is to force an animation update)
- The follower's
chompy
following¶
chompy
is a special kind of follower because it's not part of the main player party, but a regular tempfollowers
that simply keeps reappearing on each map load. This is acomplished by logic in the first LateUpdate (known as latestart
) that happens right after the main followers logic described above happens.
It only happens if flag 402 is true (Chompy is with Team Snakemouth) and the player
isn't in a submarine
. If these conditions are met, the following happens:
- AddFollower is called without a caller and with 169 (
ChompyChan
) as id - The map's
chompy
is set to the follower entity just added chompy
'stempfollowerid
is set to the matching index oftempfollowers
chompy
'songround
is set to false (this is to force an animation update)
Issues with followers's tempfollowerid
¶
An entity's tempfollowerid
field is intended to express the index of the follower in the map's tempfollowers
array. It tracked purely for EntityControl to perform anti z fighting measures during the Follow logic when the player
is flying
.
The problem is this isn't always assigned correctly because MainManager.AddFollower doesn't do this so it relies on the caller to do it and not all callers do it correctly. In the case of the TestRoom
map, it's not assigned at all so z figthing can happen when the player
is flying
still.
More confusingly however is the normal case of adding followers other than chompy
: in this case, MapControl incorrectly sets the follower's tempfollowerid
to the index of the matching animid found in canfollowID
. The problem is canfollowID
's order isn't guaranteed by this point to give matching indexes because an unknown amount of followers might not have existed in instance.extrafollowers
. What this means is that it's possible that 2 followers ends up with the same tempfollowerid
which would cause them to z fight when the player
is flying
. Whether or not this can happen depends very specifically on the order of canfollowID
as specified in the map's prefab (after AreaSpecific modified it if needed) and which specific animids are present in instance.extrafollowers
.
Issue regarding follower order consistency¶
There's another different issue the follower system has, but it's more a design issue than a programming mistake: the order followers are in can change between the moment the follower is added and the next map load.
This is because AddFollower ALWAYS append the follower to the end of tempfollower
and it always becomes the last entity in the follow chain. This isn't consistent with how MapControl builds this chain on latestart
: it adds the requested ones in the order they appear in canfollowID
and then adds chompy
if needed.
It means for example that a follower could be following chompy
when added after the map's latestart
(which is frequently the case in events), but when loading a map right next to the current one, the follower won't be following chompy
and rather, chompy
will be the last follower. This inconsistency can be unexpected and since it's not possible to tell where the follower should be inserted in the chain, it can always happen when calling AddFollower after the map's latestart
.
Configuring the entity's follow logic¶
There are 2 ways MapControl can configure the actual follow logic entities have: followerylimit
and closemove
.
followerylimit
¶
This configuration field controls the maximum y distance followers are allowed to be from their followee before they get teleported to their followee. It's enforced by EntityControl's DoFollow.
It should be noted that there's another entity field involved in this teleport logic: the entity's followlimit
field. This field is almost always 20.0 with the only exception being the Beemerang entity where it is 6.0. That one enforces the maximum square distance allowed in x/z before the teleportation happen.
They both enforce the same thing, but only one limit being violated is enough for the teleport to occur.
closemove
¶
The entity following logic has a different mode of operation called CloseMove. More details can be learned in the follow move logic documentation, but it's essentially a method on EntityControl that if it returns true, it will cause entities to follow their followee twice as close than normal.
The map's closemove
configuration field is involved in the CloseMove check, but in a rather complex fashion. Having this field set to true may implicate the CloseMove logic, but it's not enough on its own to enforce it.
First off, no matter what the value of this field is, CloseMove requires that that the player
exists and isn't dashing
for the logic to be used. If these conditions are met, then the logic still apply if the player
's forceclosemove
is true no matter what the map's closemove
says. Additionally, if the entity isn't part of the mainparty
then whether or not the logic apply only depends on the player
's forceclosemove
so the map's closemove
isn't involved. Also, even if it is part of the mainparty
, the player
still needs to be rooted to the scene as otherwise, it entirely depends on the player
's forceclosemove
once again (the player
is normally rooted, but it can be on certain platform that childs the player
to them). Finally, even if all of the above don't apply, but the map's closemove
is true, then flags 401 (the "stealth mode" flag) needs to be true as otherwise, it once again entirely depends on the player
's forceclosemove
.
To summarise, the configutation field only has an impact if all of the following are true:
- The
player
exists and isn'tdashing
(otherwise, the logic never applies) - The entity is part of the
mainparty
(otherwise, theplayer
'sforceclosemove
decides everything which ignores the map'sclosemove
) - The
player
is rooted to the scene (othereise, same outcome: theplayer
'sclosemove
decides everything) - The flag 401 is true (otherwise, same outcome: the
player
'sclosemove
decides everything) - The
player
'sforceclosemove
is false (otherwise, the configuation field wouldn't change that the logic would apply sinceforceclosemove
takes precedence over it)
What's effectively happening is that in order to force the CloseMove logic to happen, there's 2 ways:
- The
player
'sforceclosemove
is true: this takes precedence over everything such that CloseMove always applies unless either theplayer
doesn't exist or it isdashing
. In this case, the map'sclosemove
is effectively ignored. However, it should be noted that under normal gameplay, this only happens when theplayer
is on top of a TempPlatform - If the above doesn't apply, BOTH the map's
closemove
AND flag 401 needs to be true to guarantee that the CloseMove logic will happen alongside the other conditions mentioned further above
The flag 401 check is what makes the influence of the map's closemove
more complicated because they are both managed very inconsistently with each other.
Exploring further the issue with closemove
¶
While closemove
is a configuration field, it can be overriden by AreaSpecific:
- It is always overriden to true in the
WaspKingdom
area except for a list of specific maps. In this case, flag 401 (and alsocantcompass
) will be set to true ifclosemove
was also overriden to true. Therefore, the CloseMove logic always consistently apply in theWaspKingdom
except for the following maps (it covers all maps used in the stealth section during chapter 5):WaspKingdomOutside
WaspKingdomJayde
WaspKingdomMainHall
WaspKingdomPrison
WaspKingdom5
WaspKingdomQueen
WaspKingdomThrone
- In the
GiantLair
area,closemove
isn't overriden, but flag 401 has its value set to it. The maps in this area correctly haveclosemove
defined to true when needed so it means this stealth section is consistently forcing the CloseMove logic to apply closemove
is always overriden to false in any areas other thanWaspKingdom
orGiantLair
. This is consistent with the above, but it's not with what is explained below
There is one stealth section in the game where the CloseMove logic isn't applied consistently: the BanditHideout
area. This one has the following management:
- Event 109 will set flag 401 to true at the
HideoutCentralRoom
map towards the end of the event and set it back to false at the same event inHideoutWestStorage
closemove
is set to true on all theBanditHideout
maps contained in the stealth section
This should mean that the CloseMove logic should be consistent in this section. The problem is AreaSpecific overrides closemove
to false at BanditHideout
because it's not correctly included in that logic where its value shouldn't change there. This leads to the unexpected behavior that while the CloseMove logic applies right when event 109 ends, it will immediately stop to apply the moment another map is loaded from HideoutCentralRoom
. This can be seen by observing the party's follow distance incorrectly changing.