Contents
- 1 About
- 2 Mods
- 3 Experience
- 3.1 Countless Hours Wasted
- 3.2 Nice to know
- 3.3 Unreal Tournament 2004 - Compiler Errors
- 3.4 Unreal Tournament 2004 Bugs || Errors
- 3.4.1 Servers do not know the locations of spectators
- 3.4.2 xPawn.ChangedWeapon breaks simulated function chain
- 3.4.3 Players who are out can sometimes respawn
- 3.4.4 AskforPawn and ClientGotoState
- 3.4.5 Sometimes a player pawn spawns and gets possessed, but immediately gets destroyed again
- 3.4.6 InitTeamSymbols may sometimes give both teams the same team symbol
- 3.4.7 ToggleScreenShotMode does not restore a variety of state
- 3.4.8 TakeDrowningDamage
- 3.4.9 PlayTakeHit never played drowning sound
- 4 Tips
About
Haphazard hobbyist programming sessions since about 1992.
Articles
User:Rejecht/Key_Bindings_(UT2004)
Where
Other
Websites
- rejecht on Google Sites - rejecht's notes
UE 4
User:Rejecht/Getting Started UE4
Mods
An overview of mods (game types, maps, mutators) can be found on rejecht's notes.
Experience
Countless Hours Wasted
Demo Recording & Playback
- Playing back a demo recorded in a networked game results in several text messages being duplicated. For example, orders sent to bots, so it was never a bug in the mod, but my lack of awareness of this issue in the first place.
Nice to know
ClientFlash does nothing
(It's used to fade all screen colors from a starting color.)
Check these dependencies:
- ScreenFlashes in UT2004.ini must be True for the driver you use.
- Check (ScreenFlashScaling > 0.0f) in User.ini.
If the settings check out, check if something prevents PlayerController.ViewFlash () from getting called.
Having no pawn is the same as being in a Dead state
The game has a hard-coded client-side rule that forces the player to enter a dead state when it does not find a Pawn.
Triggers:
- [NM_Standalone|NM_ListenServer|NM_Client] (Pawn == None)
Cause:
- PlayerController.(PlayerWalking)PlayerMove ()
NumLives is not the number of lives
NumLives is the number of deaths (the number of lives used).
Errors found in Unreal Engine 2.5 may also be present in the UDK
Unreal Tournament 2004 - Compiler Errors
General Protection Fault
History:
- FScriptCompiler::CompileConst
- <- Const
- <- FScriptCompiler::CompileDeclaration
- <- FScriptCompiler::CompileStatement
- <- FirstPass
- <- TryCompile
- <- FScriptCompiler::CompileScript
- <- (Class PackageName.ClassName, Pass 0, Line %Line_Number%)
- <- MakeScript <- MakeScript <- MakeScript <- MakeScript <- MakeScript <- MakeScript <- MakeScript <- MakeScript <- MakeScript
- <- DoScripts
- <- UEditorEngine::MakeScripts
- <- UMakeCommandlet::Main
Cause:
- Declaring the same variable name as const value twice.
File content example:
class Example extends Actor; const crash = 1; const crash = 2; // Cause of crash.
ImportText && "Bad termination in:"
Problem:
- A dynamic array with a lot of elements will not be stored correctly in the resulting int file.
Cause:
- The compiler cuts off the dynamic array after 4096 characters. After the 4096th character, the int file will contain garbage. This is in turn logged by ImportText upon accessing the dynamic array as "Bad termination in:" at runtime.
Trivia:
- Elements in a dynamic array are serialized to a single line in an int file.
- Elements in a static array are serialized to a single line per element.
- The compiler probably uses a static text buffer of 4096 characters per line.
Solutions:
- Partition up the dynamic array into groups of dynamic arrays (acceptable).
- Change over to a static array - replace Count = ArrayVariableName.Length with Count = ArrayCount (ArrayVariableName).
Unreal Tournament 2004 Bugs || Errors
Servers do not know the locations of spectators
Spectators are in control of the location, and do not get sneaky state information, nor do they send location information to the server in the same way as a player with a pawn does.
The sources for UT3 1.5 try to solve this problem with ServerSetSpectatorLocation, however, this function can't possibly work as intended, as it does not update the position when the camera moves, but rather sets the last position and stays there. To track the camera, use PlayerCalcView, and throttle calls to your own ServerSetSpectatorLocation from the client (no need to call each tick). You also need to make sure PlayerMove (movement keys) does not offset the view from where the camera is, unless the player has freed the camera.
It is assumed that this has something to do with sounds not playing in the right location, but not yet proven.
xPawn.ChangedWeapon breaks simulated function chain
Pawn.ChangedWeapon is declared simulated, however, in the derived xPawn class, it is redeclared non-simulated while still being called from simulated functions, thus breaking the chain until the Pawn becomes ROLE_AutonomousProxy, which may take some time in netplay.
Players who are out can sometimes respawn
Triggers for this error:
- It must be a TeamGame.
- (MaxLives > 0) - Assume that this example has (MaxLives == 1)
- TeamGame.ScoreKill has the errors.
By design?
- Team kills are not counted as deaths, thus giving the player another chance. This might be desirable in some game types, however, a simple solution is to track the NumLives of the killed player before and after the TeamGame.ScoreKill call; if NumLives is unchanged, decide whether to call Super(DeathMatch).ScoreKill to deal with this, depending on whether this behavior is wanted.
Consequences:
- The player can respawn despite having died the allotted MaxLives.
Cause:
- The ScoreKill logic in TeamGame does not always call Super.ScoreKill, which is the one handling the NumLives count and sets bOutofLives accordingly. This is by design, but not necessarily desirable.
First attempt:
- OK, so let's just override ScoreKill and increment NumLives ourselves, before calling ScoreKill, since the DeathMatch implementation expects (NumLives >= MaxLives) and sets bOutofLives accordingly.
First consequence of the first attempt: While this solved the original problem, it also introduced another problem:
- This triggers LateEntryLives (which defaults to 1), forcing latecomers to become spectators (bOnlySpectator) at login, and some people losing their hair because they now have a new unwanted behaviour.
This is because some players now get (NumLives == 2), and others get (NumLives == 1), depending on the circumstances in TeamGame.ScoreKill. Login checks LateEntryLives against all players and boots the player to a spectator slot when it finds (NumLives (2) > LateEntryLives (1)).
Other possible approaches to solve the first consequence:
- Bad: Set LateEntryLives to MAXINT in InitGame to override any ini setting, unless you want to use LateEntryLives.
- Bad: Preincrement NumLives before Super.ScoreKill and clamp the value postcall (the first attempt).
The best solution is to track changes in NumLives and act accordingly after a Super.ScoreKill call to the TeamGame implementation. Also, add a choice whether to trigger this behavior, in case someone actually wants the default behavior.
AskforPawn and ClientGotoState
AskforPawn insists on calling ClientGotoState ('Spectating', 'Begin'), but there is no Begin: label in the Spectating state, which leads to an error.
The UDK solves this in the ClientGotoState function, but this code base also gets rid of the logic forcing the client to a spectating state.
Sometimes a player pawn spawns and gets possessed, but immediately gets destroyed again
(First, figuring out how to reproduce the error took some time, then the technique to trigger it again during testing. This one took 3/4 of a day to figure out and solve; logging/tracing client-side and server-side state changes step by effing step.)
So, sometimes, when a player presses the Fire button, just right after a controller enters one of the Player* states (PlayerWalking), ServerSpectate gets called, which destroys the pawn, enters a spectating state, and then ignores ClientRestart calls.
The first solution is simply to ignore ServerSpectate in the Player* states in PlayerController; such as PlayerWaiting, PlayerWalking, WaitingforPawn.
InitTeamSymbols may sometimes give both teams the same team symbol
DeathMatch.InitTeamSymbols is replaced with the following implementation.
Instead of getting in trouble with more random numbers, it just picks the next team symbol in the array, wrapping around to zero.
(Index_MaxTeams == 2)
The (Count_TeamSymbols < 10) check is an early warning system for someone who managed to change the team symbols--this implementation is adapted for at least ten different team symbols.
// *UT2004Fix* // (Replaces) DeathMatch.InitTeamSymbols // - Sometimes both teams would get the same team symbol. event InitTeamSymbols () { local array<string> Group_Text_TeamSymbol; local int Count_TeamSymbols; local int Index_TeamSymbol[Index_MaxTeams]; local Texture Texture_TeamSymbol; // ** Attempt to get a list of team symbols. if ( (GameReplicationInfo.TeamSymbols[0] != None) && (GameReplicationInfo.TeamSymbols[1] != None) ) { return; // Already loaded. } // ** Team Symbol Names Class'CacheManager'.Static.GetTeamSymbolList (Group_Text_TeamSymbol, true); Count_TeamSymbols = Group_Text_TeamSymbol.Length; if (Count_TeamSymbols < 10) { Warn ("(Count_TeamSymbols < 10)"); return; } // ** Randomization Index_TeamSymbol[0] = Rand (Count_TeamSymbols - 1); Index_TeamSymbol[1] = Rand (Count_TeamSymbols - 9); // ** // - Handle Equality if (Index_TeamSymbol[0] == Index_TeamSymbol[1]) { ++Index_TeamSymbol[1]; if (Index_TeamSymbol[1] >= Count_TeamSymbols) { Index_TeamSymbol[1] = 0; } } // ** Red Team Texture_TeamSymbol = Texture(DynamicLoadObject (Group_Text_TeamSymbol[Index_TeamSymbol[0]], Class'Texture')); if (Texture_TeamSymbol != None) { GameReplicationInfo.TeamSymbols[0] = Texture_TeamSymbol; } else { Warn ("(Texture_TeamSymbol[0] == None)"@Group_Text_TeamSymbol[Index_TeamSymbol[0]]); } // ** Blue Team Texture_TeamSymbol = Texture(DynamicLoadObject (Group_Text_TeamSymbol[Index_TeamSymbol[1]], Class'Texture')); if (Texture_TeamSymbol != None) { GameReplicationInfo.TeamSymbols[1] = Texture_TeamSymbol; } else { Warn ("(Texture_TeamSymbol[1] == None)"@Group_Text_TeamSymbol[Index_TeamSymbol[1]]); } // ** Replication GameReplicationInfo.TeamSymbolNotify (); }
ToggleScreenShotMode does not restore a variety of state
Scope:
- PlayerController.ToggleScreenShotMode
Type:
- Save/Restore
Fix:
- Remember and restore Handedness, bHideVehicleNoEntryIndicator.
TakeDrowningDamage
Triggers:
- Pawn is in a water volume and the match has entered the MatchOver state.
Scope:
- Pawn.TakeDrowningDamage
Type:
- Access Error
Fix:
- Check (Controller != None) before calling Super.TakeDrowningDamage in a subclass of pawn.
PlayTakeHit never played drowning sound
Scope:
- xPawn.PlayTakeHit
Type:
- Class Check Error
Corrections:
- Find: DamageType.IsA('Drowned')
- Replace With: DamageType == Class'Drowned'
Reminder:
- Do not use IsA with a metaclass reference (Syntax: Class<Classname>).
- Use IsA with an instance of a class.
Trivia:
- I don't think the actual drowning sounds were included anyway.
Tips
Example: Using a bitwise and operator
Theory: You can check if a number is even {0, 2, 4, ...} or odd (1, 3, 5, ...) by testing the least significant binary digit of an integer (two's complement). If it's 0, this is an even number (0, 2, 4, ...). If it's 1, this is an odd number {1, 3, 5, ...}.
function Example () { local int Index; for (Index = 0; Index < 4; ++Index) { if ((Index & 1) == 0) // Test if the least significant bit is cleared. { Log ("["$Index$"] Even"); } if ((Index & 1) == 1) // Test if the least significant bit is set. { Log ("["$Index$"] Odd"); } } } // Output: // ScriptLog: [0] Even // ScriptLog: [1] Odd // ScriptLog: [2] Even // ScriptLog: [3] Odd
Example: Using a bitwise and operator II
function Example () { local int Index; for (Index = 0; Index < 8; ++Index) { if ((Index & 2) == 2) // Test if bit #2 is set. You have to compare using powers of two. { Log ("["$Index$"] Set"); } } } // Output: // ScriptLog: [2] Set // ScriptLog: [3] Set // ScriptLog: [6] Set // ScriptLog: [7] Set
Example: Using a bitwise and operator III
To show that you are not testing against the bit number, but a value. You have to compare using powers of two.
function Example () { local int Index; Index = 255; if ((Index & 128) == 128) // Test if bit #7 (2^7 = 128) is set. You have to compare and check using powers of two. { Log ("["$Index$"] Set"); // Bit #7 was set } else { Log ("["$Index$"] Cleared"); // Bit #7 was cleared } } // Output: // ScriptLog: [255] Set
Example: Using a bitwise or operator
Theory: The color-coded text strings have the format 0x1B (ANSI Escape Code) followed by three bytes representing the red, green, and blue channels, where each channel must be in the range [1, 255] because the value 0 (NUL) terminates the string. The function in this example takes a color and uses the bitwise or operator to make sure that the value of each color channel is always at least 1. Note that there is no alpha channel (the string is opaque). Note that StrLen counts color codes.
Also note that this is a bad real-world example, because all channel values are always odd numbers. For example, if the red channel value is 50, the color code becomes 51. See the second example for the real world use of such a function, or you care about accuracy.
function string GetText_ColorCode (Color Color) { return ( Chr (0x1B) $ Chr (Color.R | 1) $ Chr (Color.G | 1) $ Chr (Color.B | 1) ); }
For the sake of completeness, a version that applies to the real world, using Max to keep the value within the allowed range [1, 255].
function string GetText_ColorCode (Color Color) { return ( Chr (0x1B) $ Chr (Max (Color.R, 1)) $ Chr (Max (Color.G, 1)) $ Chr (Max (Color.B, 1)) ); }