DoomDoctor
Table of Contents
DoomDoctor contains tools for GZDoom mod debugging.
DoomDoctor is a part of DoomToolbox.
1. Acknowledgments
- Thanks to KeksDose for the concept of VM abort handler.
- Thanks to Colerx for bug reports.
- Thanks to Accensus for feature suggestions.
- Thanks to ZippeyKeys12 for Clematis.
2. Licenses
3. Tests preamble
Notes:
- Don't decrease the delay before reviving, the actor sometimes isn't fully dead yet. TODO: make the scripts wait until something happens.
- Triggering several events at once to speed up tests. The events should not interfere with each other.
- Warp and use commands are supposed to activate the Miniwad end level button.
dd_logging_engine_events_enabled true; dd_logging_replace_events_enabled true; dd_logging_world_events_enabled true; dd_logging_player_events_enabled true; dd_logging_process_events_enabled true; wait 2; map map01; wait 4; dd_force_lightning; wait 5; event TestConsoleEvent; wait 6; interfaceEvent TestInterfaceEvent 9; wait 7; mdk TestDamageType; wait 8; kill; wait 11; resurrect; warp 60 -128 0; +use; wait 14; quit
4. Source code preamble
Note: dd_StringUtils.zs must be installed externally.
5. VM Abort Handling
TODO: move VM abort handling to a module.
VM abort reports:
- system time;
- basic game information: map name, total time, multiplayer status, player class, skill;
- game configuration: compat flags, dm flags, autoaim;
- event handler list;
- a request for the user to report the bug.
If there are several VM abort handlers loaded, only the first one will print stuff.
For this to work, all handlers must have VmAbortHandler somewhere in their class
name.
5.1. Settings
user bool dd_vm_abort_report_enabled = true;
5.2. Console commands
Alias dd_report "event dd_report"
5.3. dd_VmAbortHandler
TODO: make dd_VmAbortHandler savefile-compatible (StaticEventHandler).
class dd_VmAbortHandler : EventHandler { override void playerSpawned(PlayerEvent event) { mReport = new("dd_Report"); if (event.playerNumber == consolePlayer) mReport.writePlayerInfo(); } override void uiTick() { bool isOnceASecond = level.totalTime % TICRATE == 0; if (isOnceASecond) mReport.writeSystemTime(); } override void onDestroy() { if (gameState != GS_FullConsole || !amIFirst() || !Cvar.getCvar("dd_vm_abort_report_enabled", players[consolePlayer]).getBool()) { return; } Console.printf("%s\n%s", mReport.report(), getAttentionMessage()); } override void consoleProcess(ConsoleEvent event) { if (amIFirst() && event.name == "dd_report") { Console.printf("%s", mReport.report()); } } private clearscope bool amIFirst() { foreach (aClass : AllClasses) { string className = aClass.getClassName(); bool isVmAbortHandler = (className.indexOf("VmAbortHandler") != -1); if (!isVmAbortHandler) continue; return className == getClassName(); } return false; } private clearscope string getAttentionMessage() { string userName = players[consolePlayer].getUserName(); string hashes = "\cg############################################################"; Array<string> lines = { "", hashes, " " .. userName .. "\cg, please report this VM abort to mod author.", " Attach screenshot to the report.", " Type \"screenshot\" below to take a screenshot.", hashes }; return dd_su.join(lines, "\n"); } private dd_Report mReport; } // class dd_VmAbortHandler
5.4. dd_Report
class dd_Report { clearscope void writePlayerInfo() { mPlayerClassName = players[consolePlayer].mo.getClassName(); mSkillName = g_SkillName(); } ui void writeSystemTime() { mSystemTime = SystemTime.now(); } clearscope string report() { Array<string> lines = { "DoomDoctor Report: " .. getSystemTime(), getGameInfo(), getConfiguration(), getEventHandlers() }; return dd_su.join(lines, "\n"); } private static clearscope string getConfiguration() { return new("dd_Description") .addCvar("compatflags") .addCvar("compatflags2") .addCvar("dmflags") .addCvar("dmflags2") .addCvar("autoaim").compose(); } private clearscope string getGameInfo() { return new("dd_Description") .add("level", level.mapName) .addInt("time", level.totalTime) .addBool("multiplayer", multiplayer) .add("player class", mPlayerClassName) .add("skill", mSkillName).compose(); } private static clearscope string getEventHandlers() { Array<string> normalEventHandlers; Array<string> staticEventHandlers; foreach (aClass : AllClasses) { if (!(aClass is "StaticEventHandler")) continue; if (aClass == "StaticEventHandler" || aClass == "EventHandler") continue; if (aClass is "EventHandler") normalEventHandlers.push(aClass.getClassName()); else staticEventHandlers.push(aClass.getClassName()); } return "Event handlers: " .. dd_su.join(normalEventHandlers) .. "\n" .. "Static event handlers: " .. dd_su.join(staticEventHandlers); } private clearscope string getSystemTime() { return "System time: " .. SystemTime.format("%F %T %Z", mSystemTime); } private string mPlayerClassName; private string mSkillName; private int mSystemTime; } // class dd_Report
6. Troublemaker
Troublemaker provides console commands to check if a mod can handle some unexpected events.
6.1. Console commands
6.1.1. Commands to cause problematic events
Alias dd_nullify_player "netevent dd_nullify_player" Alias dd_spawn_null_thing "netevent dd_spawn_null_thing; summon dd_Spawnable" Alias dd_nullify_player_weapon "netevent dd_nullify_player_weapon" Alias dd_take_all_weapons "take weapons" Alias dd_spawn_with_no_tags "summon dd_WeaponWithNoTag; summon dd_EnemyWithNoTag"
6.1.2. Helper commands
Alias dd_revive_everything "netevent dd_revive_everything" Alias dd_force_lightning "netevent dd_force_lightning"
6.2. Source
TODO: make dd_Troublemaker savefile-compatible (StaticEventHandler).
mixin class dd_Volatile { override void Tick() { if (GetAge() > 0) destroy(); } } class dd_WeaponWithNoTag : Weapon { mixin dd_Volatile; } class dd_Spawnable : Actor { mixin dd_Volatile; } class dd_EnemyWithNoTag : Actor { Default { +IsMonster; } mixin dd_Volatile; } class dd_Troublemaker : EventHandler { // To be able to change events before they are processed by other event handlers. override void OnRegister() { setOrder(int.min); } override void NetworkProcess(ConsoleEvent event) { string command = event.name; if (command == "dd_nullify_player") nullifyPlayer(); else if (command == "dd_spawn_null_thing") nullifySpawnedThing(); else if (command == "dd_nullify_player_weapon") nullifyPlayerWeapon(); else if (command == "dd_revive_everything") reviveEverything(); else if (command == "dd_force_lightning") forceLightning(); } override void WorldThingSpawned(WorldEvent event) { if (mIsScheduledSpawnedThingIsNull) { mIsScheduledSpawnedThingIsNull = false; event.thing.destroy(); } } private void nullifyPlayer() { players[consolePlayer].mo.destroy(); // Interestingly, the //players[consolePlayer].mo = NULL; // just crashes GZDoom. Don't ever do that! } private void nullifySpawnedThing() { mIsScheduledSpawnedThingIsNull = true; } private void nullifyPlayerWeapon() { players[consolePlayer].readyWeapon = NULL; } private void reviveEverything() { Actor anActor; for (let i = ThinkerIterator.Create("Actor"); anActor = Actor(i.Next());) { players[consolePlayer].mo.RaiseActor(anActor); } } // TODO: test on a map with lightning. private void forceLightning() { let lightningIterator = ThinkerIterator.Create("Thinker", Thinker.STAT_Lightning); bool wasLightning = lightningIterator.Next() != NULL; if (wasLightning) level.ForceLightning(0); else level.ForceLightning(1); } private bool mIsScheduledSpawnedThingIsNull; } // class dd_Troublemaker
7. Logging
7.1. Settings
server bool dd_logging_engine_events_enabled = false; server bool dd_logging_replace_events_enabled = false; user bool dd_logging_world_events_enabled = false; user bool dd_logging_player_events_enabled = false; user bool dd_logging_process_events_enabled = false;
7.2. Console commands
Alias dd_logging_disable "ResetCvar dd_logging_engine_events_enabled; ResetCvar dd_logging_replace_events_enabled; ResetCvar dd_logging_world_events_enabled; ResetCvar dd_logging_player_events_enabled; ResetCvar dd_logging_process_events_enabled"
7.3. dd_BufferedConsole
Prints to the engine console and saves the messages so they can be checked. Also prints level time.
StaticEventHandler used as a Singleton.
class dd_BufferedConsole : StaticEventHandler { static clearscope dd_BufferedConsole getInstance() { return dd_BufferedConsole(find("dd_BufferedConsole")); } static clearscope void printf(string format, string arg1 = "", string arg2 = "") { string message = string.format(format, arg1, arg2); getInstance().append(message); Console.printf("(%05d) %s", level.time, message); } void append(string message) const { mBuffer.appendFormat("\n" .. message); } void clear() const { mBuffer = ""; } bool contains(string substring) const { return mBuffer.IndexOf(substring) != -1; } private string mBuffer; } // class dd_BufferedConsole
7.4. dd_Logger
Notes
- The following events are not logged, because nothing interesting can change here:
RenderOverlay,RenderUnderlay,UiTick,PostUiTick,InputProcess,UiProcess. - Events cannot be destroyed, so event parameters are never NULL.
- Most events are followed by the test code that also works as an example of what an event report contains.
class dd_Logger : StaticEventHandler
7.4.1. Engine events
OnRegister
override void OnRegister() { if (!dd_logging_engine_events_enabled) return; // To catch all changes to events. setOrder(int.max - 1); mFunctionName = "OnRegister"; logInfo(); }
void OnRegisterTest() { assert("log: OnRegister", mConsole.contains("OnRegister")); mConsole.clear(); }
OnUnregister
override void OnUnregister() { if (!dd_logging_engine_events_enabled) return; mFunctionName = "OnUnregister"; logInfo(); }
Note: event order for
OnUnregisteris reversed.OnEngineInitialize
override void OnEngineInitialize() { if (!dd_logging_engine_events_enabled) return; mFunctionName = "OnEngineInitialize"; logInfo(); }
override void OnEngineInitialize() { assert("log: OnEngineInitialize", mConsole.contains("OnEngineInitialize")); mConsole.clear(); }
NewGame
override void NewGame() { if (!dd_logging_engine_events_enabled) return; mFunctionName = "NewGame"; logInfo(); }
override void NewGame() { if (mOnlyOnceFlag0) return; mOnlyOnceFlag0 = true;; assert("log: NewGame", mConsole.contains("NewGame")); mConsole.clear(); }
7.4.2. World events
WorldLoaded
override void WorldLoaded(WorldEvent event) { // To load Cvars when the game is loaded from a save. loadCvars(); if (!dd_logging_world_events_enabled.getBool()) return; mFunctionName = "WorldLoaded"; logInfo(describeWorldEvent(event, IsSaveGame | IsReopen)); check(OtherHandlers | PlayerChecks, event); }
override void WorldLoaded(WorldEvent event) { if (mOnlyOnceFlag1) return; mOnlyOnceFlag1 = true;; assert("log: WorldLoaded", mConsole.contains("WorldLoaded")); assert("log: WorldLoaded", mConsole.contains("IsSaveGame: false")); assert("log: WorldLoaded", mConsole.contains("IsReopen")); mConsole.clear(); }
WorldUnloaded
override void WorldUnloaded(WorldEvent event) { if (!dd_logging_world_events_enabled.getBool()) return; mFunctionName = "WorldUnloaded"; logInfo(describeWorldEvent(event, IsSaveGame | NextMap)); }
Note: event order for
WorldUnloadedis reversed.WorldThingSpawned
override void WorldThingSpawned(WorldEvent event) { if (!dd_logging_world_events_enabled.getBool()) return; mFunctionName = "WorldThingSpawned"; logInfo(describeWorldEvent(event, Thing)); check(PlayerChecks | ThingNull | NoTag, event); }
override void WorldThingSpawned(WorldEvent event) { if (mOnlyOnceFlag2) return; mOnlyOnceFlag2 = true;; assert("log: WorldThingSpawned", mConsole.contains("WorldThingSpawned")); assert("log: WorldThingSpawned", mConsole.contains("Thing: ")); mConsole.clear(); }
WorldThingDied
override void WorldThingDied(WorldEvent event) { if (!dd_logging_world_events_enabled.getBool()) return; mFunctionName = "WorldThingDied"; logInfo(describeWorldEvent(event, Thing | Inflictor)); check(PlayerChecks | ThingNull, event); }
The player is killed by console commands in 3 section.
override void WorldThingDied(WorldEvent event) { assert("log: WorldThingDied", mConsole.contains("WorldThingDied")); assert("log: WorldThingDied", mConsole.contains("DoomPlayer")); assert("log: WorldThingDied", mConsole.contains("Inflictor: DoomPlayer")); mConsole.clear(); }
WorldThingGround
override void WorldThingGround(WorldEvent event) { if (!dd_logging_world_events_enabled.getBool()) return; mFunctionName = "WorldThingGround"; logInfo(describeWorldEvent(event, Thing | CrushedState)); check(PlayerChecks | ThingNull, event); }
TODO: how to test this?
WorldThingRevived
override void WorldThingRevived(WorldEvent event) { if (!dd_logging_world_events_enabled.getBool()) return; mFunctionName = "WorldThingRevived"; logInfo(describeWorldEvent(event, Thing)); check(PlayerChecks | ThingNull, event); }
The player is resurrected by console commands in 3 section.
override void WorldThingRevived(WorldEvent event) { assert("log: WorldThingRevived", mConsole.contains("WorldThingRevived")); assert("log: WorldThingRevived", mConsole.contains("DoomPlayer")); mConsole.clear(); }
WorldThingDamaged
override void WorldThingDamaged(WorldEvent event) { if (!dd_logging_world_events_enabled.getBool()) return; mFunctionName = "WorldThingDamaged"; logInfo(describeWorldEvent(event, Thing | Inflictor | DamageProperties | DamageFlags | DamageAngle)); check(PlayerChecks | ThingNull, event); }
The player is damaged by console commands in 3 section.
override void WorldThingDamaged(WorldEvent event) { if (mOnlyOnceFlag3) return; mOnlyOnceFlag3 = true;; assert("log: WorldThingDamaged", mConsole.contains("WorldThingDamaged")); assert("log: WorldThingDamaged", mConsole.contains("DoomPlayer")); assert("log: WorldThingDamaged", mConsole.contains("Suicide")); mConsole.clear(); }
WorldThingDestroyed
override void WorldThingDestroyed(WorldEvent event) { if (!dd_logging_world_events_enabled.getBool()) return; mFunctionName = "WorldThingDestroyed"; logInfo(describeWorldEvent(event, Thing)); // Player can be null here, don't check. check(ThingNull, event); }
Note: event order for
WorldThingDestroyedis reversed.WorldLinePreActivated
override void WorldLinePreActivated(WorldEvent event) { if (!dd_logging_world_events_enabled.getBool()) return; mFunctionName = "WorldLinePreActivated"; logInfo(describeWorldEvent(event, Thing | LineProperties | ShouldActivate)); check(PlayerChecks | ThingNull, event); }
override void WorldLinePreActivated(WorldEvent event) { assert("log: WorldLinePreActivated", mConsole.contains("WorldLinePreActivated")); assert("log: WorldLinePreActivated", mConsole.contains("Thing: DoomPlayer")); assert("log: WorldLinePreActivated", mConsole.contains("ActivationType: SPAC_Use")); assert("log: WorldLinePreActivated", mConsole.contains("ShouldActivate: true")); mConsole.clear(); }
WorldLineActivated
override void WorldLineActivated(WorldEvent event) { if (!dd_logging_world_events_enabled.getBool()) return; mFunctionName = "WorldLineActivated"; logInfo(describeWorldEvent(event, Thing | LineProperties)); check(PlayerChecks | ThingNull, event); }
override void WorldLineActivated(WorldEvent event) { assert("log: WorldLineActivated", mConsole.contains("WorldLineActivated")); assert("log: WorldLineActivated", mConsole.contains("Thing: DoomPlayer")); assert("log: WorldLineActivated", mConsole.contains("ActivationType: SPAC_Use")); mConsole.clear(); }
WorldSectorDamaged
override void WorldSectorDamaged(WorldEvent event) { if (!dd_logging_world_events_enabled.getBool()) return; mFunctionName = "WorldSectorDamaged"; logInfo(describeWorldEvent(event, DamageProperties | NewDamage | DamagePosition | DamageIsRadius | DamageSector | DamageSectorPart)); check(PlayerChecks, event); }
WorldLineDamaged
override void WorldLineDamaged(WorldEvent event) { if (!dd_logging_world_events_enabled.getBool()) return; mFunctionName = "WorldLineDamaged"; logInfo(describeWorldEvent(event, DamageProperties | NewDamage | DamagePosition | DamageIsRadius | DamageLine | DamageLineSide)); check(PlayerChecks, event); }
WorldLightning
override void WorldLightning(WorldEvent event) { if (!dd_logging_world_events_enabled.getBool()) return; mFunctionName = "WorldLightning"; logInfo("no parameters"); check(PlayerChecks, event); }
override void WorldLightning(WorldEvent event) { assert("log: WorldLightning", mConsole.contains("WorldLightning")); mConsole.clear(); }
WorldTick
override void WorldTick() { mFunctionName = "WorldTick"; // Do not log: frequent event. check(PlayerChecks); }
7.4.3. Player events
PlayerEntered
override void PlayerEntered(PlayerEvent event) { if (!dd_logging_player_events_enabled.getBool()) return; mFunctionName = "PlayerEntered"; logInfo(describePlayerEvent(event)); check(PlayerChecks); }
override void PlayerEntered(PlayerEvent event) { if (mOnlyOnceFlag4) return; mOnlyOnceFlag4 = true;; assert("log: PlayerEntered", mConsole.contains("PlayerEntered")); assert("log: PlayerEntered", mConsole.contains("PlayerNumber: 0")); assert("log: PlayerEntered", mConsole.contains("IsReturn: false")); mConsole.clear(); }
PlayerSpawned
override void PlayerSpawned(PlayerEvent event) { loadCvars(); if (!dd_logging_player_events_enabled.getBool()) return; mFunctionName = "PlayerSpawned"; logInfo(describePlayerEvent(event)); check(PlayerChecks); }
override void PlayerSpawned(PlayerEvent event) { if (mOnlyOnceFlag5) return; mOnlyOnceFlag5 = true;; assert("log: PlayerSpawned", mConsole.contains("PlayerSpawned")); mConsole.clear(); }
PlayerRespawned
override void PlayerRespawned(PlayerEvent event) { if (!dd_logging_player_events_enabled.getBool()) return; mFunctionName = "PlayerRespawned"; logInfo(describePlayerEvent(event)); check(PlayerChecks); }
override void PlayerRespawned(PlayerEvent event) { assert("log: PlayerRespawned", mConsole.contains("PlayerRespawned")); mConsole.clear(); }
PlayerDied
override void PlayerDied(PlayerEvent event) { if (!dd_logging_player_events_enabled.getBool()) return; mFunctionName = "PlayerDied"; logInfo(describePlayerEvent(event)); check(PlayerChecks); }
override void PlayerDied(PlayerEvent event) { assert("log: PlayerDied", mConsole.contains("PlayerDied")); mConsole.clear(); }
PlayerDisconnected
override void PlayerDisconnected(PlayerEvent event) { if (!dd_logging_player_events_enabled.getBool()) return; mFunctionName = "PlayerDisconnected"; logInfo(describePlayerEvent(event)); check(PlayerChecks); }
TODO: test this.
7.4.4. Process events
ConsoleProcess
override void ConsoleProcess(ConsoleEvent event) { if (!dd_logging_process_events_enabled.getBool()) return; setFunctionName("ConsoleProcess"); logInfo(describeConsoleEvent(event)); check(PlayerChecks); }
override void ConsoleProcess(ConsoleEvent event) { assert("log: ConsoleProcess", mConsole.contains("ConsoleProcess")); assert("log: ConsoleProcess", mConsole.contains("Name: TestConsoleEvent")); mConsole.clear(); }
InterfaceProcess
override void InterfaceProcess(ConsoleEvent event) { if (!dd_logging_process_events_enabled.getBool()) return; setFunctionName("InterfaceProcess"); logInfo(describeConsoleEvent(event)); check(PlayerChecks); }
override void InterfaceProcess(ConsoleEvent event) { assert("log: InterfaceProcess", mConsole.contains("InterfaceProcess")); assert("log: InterfaceProcess", mConsole.contains("Name: TestInterfaceEvent")); assert("log: InterfaceProcess", mConsole.contains("Args: 9")); mConsole.clear(); }
NetworkProcess
override void NetworkProcess(ConsoleEvent event) { if (!dd_logging_process_events_enabled.getBool()) return; mFunctionName = "NetworkProcess"; logInfo(describeConsoleEvent(event)); check(PlayerChecks); }
override void NetworkProcess(ConsoleEvent event) { if (mOnlyOnceFlag6) return; mOnlyOnceFlag6 = true;; assert("log: NetworkProcess", mConsole.contains("NetworkProcess")); assert("log: NetworkProcess", mConsole.contains("Player: 0")); assert("log: NetworkProcess", mConsole.contains("IsManual: true")); mConsole.clear(); }
7.4.5. Replacement events
CheckReplacement
override void CheckReplacement(ReplaceEvent event) { if (!dd_logging_replace_events_enabled) return; mFunctionName = "CheckReplacement"; logInfo(describeReplaceEvent(event)); }
override void CheckReplacement(ReplaceEvent event) { if (mOnlyOnceFlag7) return; mOnlyOnceFlag7 = true;; assert("log: CheckReplacement", mConsole.contains("CheckReplacement")); assert("log: CheckReplacement", mConsole.contains("Replacement: NULL")); mConsole.clear(); }
CheckReplacee
override void CheckReplacee(ReplacedEvent event) { if (!dd_logging_replace_events_enabled) return; mFunctionName = "CheckReplacee"; logInfo(describeReplacedEvent(event)); }
Note: nothing is replaced, so no such event in the base game.
7.4.6. Constants
enum CheckFlags { Nothing = 1 << 0, OtherHandlers = 1 << 1, PlayerNull = 1 << 2, WeaponNull = 1 << 3, NoWeapons = 1 << 4, ThingNull = 1 << 5, NoTag = 1 << 6, }; const PlayerChecks = PlayerNull | WeaponNull | NoWeapons; enum WorldEventParameterFlags { IsSaveGame = 1 << 0, IsReopen = 1 << 1, NextMap = 1 << 2, Thing = 1 << 3, Inflictor = 1 << 4, Damage = 1 << 5, DamageSource = 1 << 6, DamageType = 1 << 7, DamageFlags = 1 << 8, DamageAngle = 1 << 9, ActivatedLine = 1 << 10, ActivationType = 1 << 11, ShouldActivate = 1 << 12, DamageSectorPart = 1 << 13, DamageLine = 1 << 14, DamageSector = 1 << 15, DamageLineSide = 1 << 16, DamagePosition = 1 << 17, DamageIsRadius = 1 << 18, NewDamage = 1 << 19, CrushedState = 1 << 20, }; const DamageProperties = Damage | DamageSource | DamageType; const LineProperties = ActivatedLine | ActivationType;
7.4.7. Private Functions
TODO: move checks to somewhere where they are move visible. TODO: add a check if weapons have icons. Filter by weapons that player can have.
private clearscope void check(int checks, WorldEvent aWorldEvent = NULL) { if (checks & OtherHandlers) checkOtherEventHandlers(); if (checks & PlayerNull) checkPlayerIsNull(); if (checks & NoWeapons) checkPlayerHasNoWeapons(); if (checks & WeaponNull) checkPlayerWeaponIsNull(); if (checks & ThingNull) checkWorldEventThingIsNull(aWorldEvent); if (checks & NoTag) checkWorldEventThingTag(aWorldEvent); } private static string describeWorldEvent(WorldEvent e, int parameters) { let d = new("dd_Description"); int p = parameters; if (p & IsSaveGame) d.addBool ("IsSaveGame", e.IsSaveGame); if (p & IsReopen) d.addBool ("IsReopen", e.IsReopen); if (p & NextMap) d.add ("NextMap", e.NextMap); if (p & Thing) d.addObject ("Thing", e.Thing); if (p & Inflictor) d.addObject ("Inflictor", e.Inflictor); if (p & Damage) d.addInt ("Damage", e.Damage); if (p & DamageSource) d.addObject ("DamageSource", e.DamageSource); if (p & DamageType) d.add ("DamageType", e.DamageType); if (p & DamageFlags) d.addDamageFlags("DamageFlags", e.DamageFlags); if (p & DamageAngle) d.addFloat ("DamageAngle", e.DamageAngle); if (p & ActivatedLine) d.addLine ("ActivatedLine", e.ActivatedLine); if (p & ActivationType) d.addSpac ("ActivationType", e.ActivationType); if (p & ShouldActivate) d.addBool ("ShouldActivate", e.ShouldActivate); if (p & DamageSector) d.addSector ("DamageSector", e.DamageSector); if (p & DamageSectorPart) d.addSectorPart ("DamageSectorPart", e.DamageSectorPart); if (p & DamageLine) d.addLine ("DamageLine", e.DamageLine); if (p & DamageLineSide) d.addInt ("DamageLineSide", e.DamageLineSide); if (p & DamagePosition) d.addVector3 ("DamagePosition", e.DamagePosition); if (p & DamageIsRadius) d.addBool ("DamageIsRadius", e.DamageIsRadius); if (p & NewDamage) d.addInt ("NewDamage", e.NewDamage); if (p & CrushedState) d.addState ("CrushedState", e.CrushedState); return d.compose(); } private static string describePlayerEvent(PlayerEvent event) { return new("dd_Description"). addInt("PlayerNumber", event.playerNumber). addBool("IsReturn", event.isReturn).compose(); } private clearscope static string describeConsoleEvent(ConsoleEvent event) { return new("dd_Description"). addInt ("Player", event.Player). add ("Name", event.Name). add ("Args", string.format("%d, %d, %d", event.Args[0], event.Args[1], event.Args[2])). addBool("IsManual", event.IsManual).compose(); } private static string describeReplaceEvent(ReplaceEvent event) { return new("dd_Description"). addClass("Replacee", event.Replacee). addClass("Replacement", event.Replacement). addBool ("IsFinal", event.IsFinal).compose(); } private static string describeReplacedEvent(ReplacedEvent event) { return new("dd_Description"). addClass("Replacee", event.Replacee). addClass("Replacement", event.Replacement). addBool ("IsFinal", event.IsFinal).compose(); } private clearscope void checkPlayerIsNull() { if (mIsPlayerNullLogged || players[consolePlayer].mo != NULL) return; setIsPlayerNullLogged(true); logError("player is NULL"); } private clearscope void checkWorldEventThingIsNull(WorldEvent event) { if (event.thing == NULL) logError("WorldEvent.thing is NULL"); } private clearscope void checkWorldEventThingTag(WorldEvent event) { Actor thing = event.thing; if (thing == NULL) return; if ((thing.bIsMonster || thing is "Weapon") && thing.getTag(".") == ".") { logWarning("class " .. thing.getClassName() .. " is missing a tag"); } } private clearscope void checkPlayerWeaponIsNull() { if (players[consolePlayer].readyWeapon != NULL) { setIsPlayerWeaponNullLogged(false); } else if (!mIsPlayerWeaponNullLogged) { setIsPlayerWeaponNullLogged(true); logError("player weapon is NULL"); } } private clearscope void checkPlayerHasNoWeapons() { let player = players[consolePlayer].mo; if (player == NULL) return; if (player.findInventory("Weapon", true) != NULL) { setIsPlayerHasNoWeaponsLogged(false); } else if (!mIsPlayerHasNoWeaponsLogged) { setIsPlayerHasNoWeaponsLogged(true); logError("player has no weapons"); } } private clearscope void checkOtherEventHandlers() { if (mAreOtherEventHandlersChecked) return; setAreOtherEventHandlersChecked(true); bool isLoggerFound = false; bool isTroublemakerFound = false; foreach (aClass : AllClasses) { if (aClass is "dd_Logger") isLoggerFound = true; if (aClass is "dd_Troublemaker") isTroublemakerFound = true; if (!(aClass is "StaticEventHandler") || aClass == "StaticEventHandler" || aClass == "EventHandler" || aClass == "dd_Logger" || aClass == "dd_Troublemaker") continue; string eventHandlerName = aClass.getClassName(); class<StaticEventHandler> eventHandlerClass = eventHandlerName; let instance = (aClass is "EventHandler") ? EventHandler.find(eventHandlerClass) : StaticEventHandler.find(eventHandlerClass); if (instance == NULL) { logWarning("event handler %s is defined but not activated in MAPINFO", eventHandlerName); continue; } int contenderOrder = instance.order; if (contenderOrder == int.max && isLoggerFound) { logWarning("can't inspect events from %s. Load DoomDoctor after it or increase event handler order", eventHandlerName); } else if (contenderOrder == int.min && !isTroublemakerFound) { logWarning("simulated troubles won't affect %s. Load DoomDoctor before it or decrease event handler order", eventHandlerName); } } } private clearscope void logError(string format, string s = "") { Console.printf("[ERROR] %s: %s.", mFunctionName, string.format(format, s)); } private clearscope void logWarning(string format, string s = "") { Console.printf("[WARNING] %s: %s.", mFunctionName, string.format(format, s)); } private clearscope void logInfo(string message = "(empty)") { Console.printf("[INFO] %s: %s.", mFunctionName, message); } // Hack to set class members from UI and data scopes. private play void setFunctionName(string n) const { mFunctionName = n; } private play void setIsPlayerNullLogged(bool b) const { mIsPlayerNullLogged = b; } private play void setIsPlayerWeaponNullLogged(bool b) const { mIsPlayerWeaponNullLogged = b; } private play void setIsPlayerHasNoWeaponsLogged(bool b) const { mIsPlayerHasNoWeaponsLogged = b; } private play void setAreOtherEventHandlersChecked(bool b) const { mAreOtherEventHandlersChecked = b; } private string mFunctionName; private bool mIsPlayerNullLogged; private bool mIsPlayerWeaponNullLogged; private bool mIsPlayerHasNoWeaponsLogged; private bool mAreOtherEventHandlersChecked; private dd_BufferedConsole console; private void loadCvars() { PlayerInfo player = players[consolePlayer]; dd_logging_world_events_enabled = Cvar.getCvar("dd_logging_world_events_enabled", player); dd_logging_player_events_enabled = Cvar.getCvar("dd_logging_player_events_enabled", player); dd_logging_process_events_enabled = Cvar.getCvar("dd_logging_process_events_enabled", player); } private Cvar dd_logging_world_events_enabled; private Cvar dd_logging_player_events_enabled; private Cvar dd_logging_process_events_enabled; } // class dd_Logger