DoomDoctor
Table of Contents
DoomDoctor contains tools for mod debugging.
DoomDoctor is a part of DoomToolbox.
1. Licenses
2. 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
3. Source code preamble
Note: dd_StringUtils.zs must be installed externally.
4. VM Abort Handling
4.1. Settings
user bool dd_vm_abort_report_enabled = true;
4.2. Console commands
Alias dd_report "event dd_report"
5. Troublemaker
Troublemaker provides console commands to check if a mod can handle some unexpected events.
5.1. Console commands
5.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"
5.1.2. Helper commands
Alias dd_revive_everything "netevent dd_revive_everything" Alias dd_force_lightning "netevent dd_force_lightning"
5.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 crashed 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
6. Logging
6.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;
6.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"
6.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
6.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
6.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(); }
6.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 2 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 2 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 2 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); }
6.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.
6.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(); }
6.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.
6.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;
6.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