Ctrl+K

Development Notes

Principles

  • Core is read-only. Do not edit L2JEvolution.HighFive source directly. All custom behaviour belongs in L2JEvolution.Engine.
  • Hook, don't patch. Attach to existing manager calls and event dispatch points. If a hook does not exist, add a minimal one to Core rather than duplicating logic in Engine.
  • One config file per module. No module reads another module's config file.
  • Fail-safe defaults. Every getProperty(key, default) call must have a safe default. A missing key logs a warning and uses the default. It does not throw.
  • Explicit init order. Engine modules are initialised in a deterministic sequence inside L2jhServerMods.checkl2jdrmods(), after all Core data tables are loaded.

Adding a Feature Module

Full checklist for a new Engine module:

  1. Create config/l2jdrmods/MyFeature.toml — all keys with inline comments explaining valid values.
  2. Create MyFeatureConfigs extends AbstractConfigspublic static fields, implement loadConfigs().
  3. Register: new MyFeatureConfigs().loadConfigs(); in ConfigsController.loadAll().
  4. Create MyFeatureManager with a static init() method, gated on the enable flag.
  5. Call MyFeatureManager.init(); in L2jhServerMods.checkl2jdrmods(), wrapped in the enable check.
  6. If the feature needs a UI, implement a CommunityBoardBypassHandler and register it in the bypass dispatch table.
// L2jhServerMods.checkl2jdrmods() — pattern for every module
if (MyFeatureConfigs.ENABLE) {
    MyFeatureManager.init();
    LOG.info("MyFeature: Enabled.");
}

Logging Convention

// Always per-class logger — never System.out.println
private static final Logger LOG = LoggerFactory.getLogger(MyClass.class);

// Startup confirmation:
LOG.info("MyFeature: Enabled. (maxValue={})", MyFeatureConfigs.MAX_VALUE);

// Non-fatal config issue — log and continue:
LOG.warn("MyFeature: key 'MaxValue' not found, using default (10).");

// Fatal — throw, do not just log:
throw new IllegalStateException("MyFeature: required key 'DatabaseTable' is missing.");

// Debug — only when actively debugging, remove before commit:
LOG.debug("MyFeature: processing entry id={}", entry.getId());

Thread Safety

Config static fields are written exactly once, during startup, before any player connections are accepted. No synchronisation is needed for reads.

For Manager classes that hold mutable runtime state:

  • Use ConcurrentHashMap for maps accessed from multiple threads.
  • Use CopyOnWriteArrayList for lists that are mostly read.
  • Use synchronized blocks for compound check-then-act operations.
  • Do not use volatile as a substitute for proper locking on compound operations.

Build Classpath for Engine

<!-- L2JEvolution.Engine/build.xml -->
<javac srcdir="java" destdir="bin" release="25"
       debuglevel="lines,vars,source" includeantruntime="false" fork="true">
    <classpath>
        <pathelement path="../L2JEvolution.HighFive/bin"/>
        <fileset dir="../L2JEvolution.HighFive/lib" includes="*.jar"/>
    </classpath>
</javac>

FunGuard Integration

FunGuard is a native C++ DLL that operates at the network protocol level, before packets reach Java. The Java side interacts with it through the Protection class in the security module.

  • Enable in config/l2jdrmods/SecuritySystem.toml: EnableFunGuard = true
  • The native DLL must be present in the server root directory alongside the JARs.
  • When disabled, the Protection class methods are no-ops — zero overhead.

Code Style

  • No raw System.out or System.err anywhere.
  • No unchecked casts. Use generics.
  • Config fields are public static — they are read-only after startup and accessed by name.
  • Manager getInstance() methods use double-checked locking or initialization-on-demand holder pattern.
  • XML parsing uses SAX or StAX, not DOM. DOM is prohibited for data files larger than ~1 MB.