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.

StringUtils

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

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

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

7.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.

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

7.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.

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

Created: 2026-01-04 Sun 07:06