Development Notes
Principles
- Core is read-only. Do not edit
L2JEvolution.HighFivesource directly. All custom behaviour belongs inL2JEvolution.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:
- Create
config/l2jdrmods/MyFeature.toml— all keys with inline comments explaining valid values. - Create
MyFeatureConfigs extends AbstractConfigs—public staticfields, implementloadConfigs(). - Register:
new MyFeatureConfigs().loadConfigs();inConfigsController.loadAll(). - Create
MyFeatureManagerwith a staticinit()method, gated on the enable flag. - Call
MyFeatureManager.init();inL2jhServerMods.checkl2jdrmods(), wrapped in the enable check. - If the feature needs a UI, implement a
CommunityBoardBypassHandlerand 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
ConcurrentHashMapfor maps accessed from multiple threads. - Use
CopyOnWriteArrayListfor lists that are mostly read. - Use
synchronizedblocks for compound check-then-act operations. - Do not use
volatileas 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
Protectionclass methods are no-ops — zero overhead.
Code Style
- No raw
System.outorSystem.erranywhere. - 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.