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.

StringUtils

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

  1. 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();
    }
    
  2. OnUnregister
    override void OnUnregister()
    {
      if (!dd_logging_engine_events_enabled) return;
    
      mFunctionName = "OnUnregister";
      logInfo();
    }
    

    Note: event order for OnUnregister is reversed.

  3. 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();
    }
    
  4. 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

  1. 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();
    }
    
  2. 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 WorldUnloaded is reversed.

  3. 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();
    }
    
  4. 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();
    }
    
  5. 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?

  6. 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();
    }
    
  7. 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();
    }
    
  8. 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 WorldThingDestroyed is reversed.

  9. 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();
    }
    
  10. 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();
    }
    
  11. 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);
    }
    
  12. 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);
    }
    
  13. 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();
    }
    
  14. WorldTick
    override void WorldTick()
    {
      mFunctionName = "WorldTick";
      // Do not log: frequent event.
      check(PlayerChecks);
    }
    

6.4.3. Player events

  1. 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();
    }
    
  2. 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();
    }
    
  3. 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();
    }
    
  4. 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();
    }
    
  5. 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

  1. 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();
    }
    
  2. 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();
    }
    
  3. 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

  1. 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();
    }
    
  2. 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

Created: 2026-03-17 Tue 16:43