Skip to content

Migration guide

This page lists the steps a host has to take when an adapter release breaks compatibility. Releases without an entry here are drop-in: bump the version and restart.

  • Every module is versioned on its own (core/v…, api/v…, db-sync/v…), and every release appears in the changelog with its notes.
  • A release that needs action from the host gets a section on this page, newest first: which module and version, what changed, and what to do.
  • The documentation always describes the current version.

nx-gs-adapter-api 0.125.0, nx-gs-adapter-core 0.54.0: APP_BAIT_LEVEL_MISMATCH, APP_NO_ROD

Section titled “nx-gs-adapter-api 0.125.0, nx-gs-adapter-core 0.54.0: APP_BAIT_LEVEL_MISMATCH, APP_NO_ROD”

Additive: a host that sends neither reason needs no change. Take both modules together: the core is what accepts the new reasons from NxAppSessions.onActionEnded.

  • New end reason LogoutReason.APP_BAIT_LEVEL_MISMATCH: the chosen bait left the hand because the character’s level left its range. The bait stack is still in the inventory; a stack that is gone stays APP_OUT_OF_BAIT.
  • New end reason LogoutReason.APP_NO_ROD: the rod left the hands, its conditions failed or it is gone.
  • APP_INTERRUPTED no longer covers lost gear. It means the game stopped the placed character’s action: it was moved off its spot, the game closed the store, or the action could not resume. A store from the app that the game closed ends with it. A host on an older core still sends APP_INTERRUPTED for lost gear until it is restarted on the new core.
  • Core 0.54.0 accepts APP_TIME_UP from the host for a session without a plan: an app store whose paid offline time ran out. A session with a plan ends by the engine’s own timer, and the core refuses APP_TIME_UP for it. Older cores ignore APP_TIME_UP from the host.
  • Send a new reason only once the platform accepts it: a new reason reaches the adapter first, then the platform, then the host.

nx-gs-adapter-api 0.124.0, nx-gs-adapter-core 0.53.0, nx-gs-runtime-sync-core 0.6.0, nx-gs-db-migrations 0.6.0: session results, item snapshots, runtime start gate

Section titled “nx-gs-adapter-api 0.124.0, nx-gs-adapter-core 0.53.0, nx-gs-runtime-sync-core 0.6.0, nx-gs-db-migrations 0.6.0: session results, item snapshots, runtime start gate”

Breaking. Take the four modules together. Switch the host in the release that takes it.

You must call NxAdapter.runtimeSync().hostStarted(), or runtime sync sends nothing. Call it once, right after you publish ServerStartedEvent, whether that event went out or is held until the events channel binds. There is no timer and no fallback: a host that never calls it gets no ticks. Call NxAppSessions.hostReady() before it.

  • CharacterPresenceEvent.charId is now characterId, in the builder and on the wire.
  • Every charId and *CharId of the api is now characterId and *CharacterId (buyerCharId to buyerCharacterId, targetCharId to targetCharacterId): commands, results, events and RatingDbDto. Builder and wire both change.
  • charName and targetCharName are now characterName and targetCharacterName, in ChatMessageEvent and TelegramCharacterLinkCommand. Builder and wire both change.
  • TransferCharToAccountCommand is now TransferCharacterToAccountCommand and TelegramCharLinkCommand is now TelegramCharacterLinkCommand, with their *Result classes. Commands route by simple class name, so Nx-Message-Type changes too: register the handler under the new class, and a sender of the old name gets UNSUPPORTED_COMMAND.
  • CharacterPresenceEvent takes result (SessionResult), on online = false only. A client session sets it through the builder; an app session gets it from the engine. activity is allowed with source = CLIENT for OFFLINE_PRIVATE_STORE only, the native offline store session.
  • OfflineSessionHost.takeSessionResult(session) is a new default method answering null. Override it to report results. The engine calls it once after every removeFromWorld, including a rolled-back start and a character that was never placed, so a recorder you keep by session id can be released there, except when the end races a graceful stop that already kept the block: that end skips the call. It also runs at a graceful stop for each session left open.
  • OfflineSession.getCarriedResult() holds the running block after a graceful stop, on the views placeInWorld and startAction get during a restore. Seed your recorder with it. An empty block means nothing was recorded; null means the block was lost, and the end will say partial.
  • A result over SessionResult.MAX_JSON_BYTES is dropped and the end goes out without it: fold uniques into items before that.
  • New values: LogoutReason.SOLD_OUT and APP_OVERWEIGHT (onActionEnded accepts the latter), and the refusals NO_FISHING_EXPERTISE, BAIT_LEVEL_MISMATCH, INVENTORY_FULL, OVERWEIGHT. INVENTORY_FULL and OVERWEIGHT carry their numbers, in CommandProblem.extensions and in AppSessionCheckResult, which requires them through its builder. APP_INVENTORY_FULL no longer covers weight.
  • Item forms carry ItemSnapshot in item in place of their loose fields, and item commands use characterId. BuyLine.attributesForMatch() is what the buy check compares: an empty map counts as null.
  • ConnectContext gains runtimeSync(): build it with the builder.
  • New key app.shutdown.result-millis-per-session in l2nx-app.properties, and a migration that adds a column to the adapter’s session table, applied at start.

nx-gs-adapter-api 0.123.0, nx-gs-adapter-core 0.52.0: chat vocabularies as enums, chat settings, whisper delivery

Section titled “nx-gs-adapter-api 0.123.0, nx-gs-adapter-core 0.52.0: chat vocabularies as enums, chat settings, whisper delivery”

Breaking (source) for Java code; the wire strings do not change. The chat constant classes become enums with the same names on the wire, and three constructors take a new parameter. Switch the host in the release that takes this api: no message changes shape, so a platform reading the old strings reads the new host as before. With the core update chat with the app is off by default: a host that publishes game chat and handles SendChatMessageCommand sets app.chat.enabled=true in its l2nx-app.properties to keep its current behaviour.

Renamed types (no deprecated copy):

Before After Notes
kafka.events.chat.WellKnownChatChannels kafka.events.chat.model.ChatChannel same constant names; ChatChannel.unknownToken(rawType) builds UNKNOWN_<int>, ChatChannel.fromToken reads a token (null when unknown)
kafka.events.chat.ChatMetadataKeys kafka.events.chat.model.ChatMetadataKey the map key is key() (source, shadowed, rawType, serverWide, undelivered), not name()
kafka.commands.chat.ChatAudiences kafka.commands.chat.model.ChatAudience same constant names

ChatMessageEvent.channel, SendChatMessageCommand.channel and .audience stay strings; their builders also take the enums. Code that put a constant string into metadata now writes ChatMetadataKey.X.key().

  • Refusal reasons. New SendChatMessageRefusalReason, written under SendChatMessageRefusal.REASON ("reason"): CHAT_FORBIDDEN (FORBIDDEN), NOT_IN_CLAN, NOT_IN_ALLIANCE, NOT_IN_PARTY (INVALID_STATE), TARGET_NOT_FOUND (NOT_FOUND), ITEM_NOT_OWNED (VALIDATION_FAILED), and the new CHAT_DISABLED, CHANNEL_DISABLED, TARGET_OFFLINE (INVALID_STATE). The strings are the ones hosts already send: replace your own reason constants with the enum’s name().
  • Chat settings. New AppRules members chatEnabled(), chatOnlineSendChannels() and chatOfflineSendChannels(), and the keys app.chat.enabled (default false), app.chat.online-send-channels (default CLAN,ALLIANCE,PARTY,WHISPER) and app.chat.offline-send-channels (default CLAN,ALLIANCE,WHISPER), see configuration. With chat off the host publishes no game chat and refuses every send from a character with CHAT_DISABLED; a command without senderCharacterId (an announcement, a line under a staff name) is not chat with the app and still goes out. A send to a channel outside the list for the character’s current state is refused with CHANNEL_DISABLED; the host picks the list by whether the character is in game when the command runs.
  • AppSettings gains a chat field (ChatSettings), a new last-but-one constructor parameter: code that builds AppSettings itself passes null or a ChatSettings.
  • SendChatMessageCommand gains offlineDelivery (Boolean), the last constructor parameter (after initiator). false: a WHISPER to an addressee who is out of game is refused with TARGET_OFFLINE and nothing is published; null (an older platform) reads as true.
  • SendChatMessageResult gains targetInGame (Boolean), the last constructor parameter. Set it on every WHISPER: true when the packet went to the addressee’s game client, false when the addressee was out of game and the message went to the app only; leave it null on other channels. The platform reads delivery from it, not from recipients.
  • New ChatMetadataKey.UNDELIVERED: the host sets it to "true" on the ChatMessageEvent of a whisper that reached no game client of its addressee (an in-game whisper to Name*, or a send from the app to an addressee out of game).

nx-gs-adapter-api 0.122.0: private buy store slots in the runtime sync

Section titled “nx-gs-adapter-api 0.122.0: private buy store slots in the runtime sync”

Breaking (source). The canonical constructor of CharacterRuntimeDto takes a new last parameter, privateStoreBuySlots (Integer): the maximum number of lines a private buy store of the character may hold right now, independent of privateStoreSellSlots. A provider computes it from the same rules as the sell limit and passes null when it does not report it. Code that builds CharacterRuntimeDto itself (providers, tests) adds the argument.

nx-gs-adapter-api 0.120.0, nx-gs-adapter-core 0.51.0, nx-gs-gd-sync-core 0.16.0, nx-gs-db-migrations 0.5.0: command initiator, session checks, fishing gear

Section titled “nx-gs-adapter-api 0.120.0, nx-gs-adapter-core 0.51.0, nx-gs-gd-sync-core 0.16.0, nx-gs-db-migrations 0.5.0: command initiator, session checks, fishing gear”

Breaking. Update the api, the core and gd-sync together and switch the host in one release.

  • Command initiator. Every command carries a required initiator (CommandInitiator): who triggered it (source is user or the sending platform service; userId is set exactly for a user; optional label, email, ip). NxCommand has getInitiator(), and every command constructor and builder takes it. The core refuses a command without a valid initiator with VALIDATION_FAILED before your handler runs, and an OwnerVerified command also needs a user source, so a handler reads a valid initiator and never re-checks it. Its ip has already passed ClientIdentifiers. Code that builds commands itself (tests, tools) passes an initiator.
  • Removed masterAccountId and initiatorIp from StartOfflineFishingCommand, StopOfflineFishingCommand, ReviveCharacterCommand and the private store start commands (StartPrivateStoreCommand), with no deprecated copy. Read initiator.getUserId() for the master account and initiator.getIp() for the request IP, and pass them on to AppSessionStartRequest as before. BanCommand.issuedBy and SendCaptchaCommand.issuedBy stay: they are the name the record shows in the game, not the attribution.
  • Package move. AppSessionStartRequest, AppSessionStartOutcome, ClientLoginOutcome, OfflineSession and OfflineSessionPlacement moved from app.l2nx.gs.adapter.api.spi.model to app.l2nx.gs.adapter.api.spi.model.session. Replace the imports; nothing else changed in them.
  • Session checks. NxAppSessions.check(characterId, spotPool, spotId) answers an AppSessionCheckOutcome (ALLOWED, CHARACTER_BUSY with the open session, SPOT_TAKEN, UNAVAILABLE) without writing anything, taking a spot or calling OfflineSessionHost. The new CheckOfflineFishingCommand (result AppSessionCheckResult) asks whether a fishing start with these inputs would start now: run your start steps up to the first side effect and call check where the start calls start. A refusal is a successful check (OK, allowed = false). Until you register a handler the adapter answers the command as unsupported. Code that implements NxAppSessions itself adds check.
  • New refusal vocabulary. AlreadyActiveScope.IP (your per-IP limit, counted under initiator.ip; an absent IP limits nothing), and the AppSessionRefusalReason values NO_FISHING_SHOT, INVALID_FISHING_SHOT and FISHING_SHOT_GRADE_MISMATCH for the new optional StartOfflineFishingCommand.fishingShotItemId. A DAILY_LIMIT refusal may now carry retryAt, the moment your daily allowance resets; leave it out when your build does not know that moment.
  • ActivityMetadataKey names the keys of an activity-usage row metadata (elapsed, remaining, rodItemId, baitItemId, fishingShotItemId, spotPool, spotId); write key() instead of your own strings. The map stays open.
  • Open-sessions snapshot. The core publishes AppSessionsSnapshotEvent (the ids of every open app session of the server) every 60 s and when the app-sessions module starts, and republishes a session end until the platform has acknowledged it. Bump nx-gs-db-migrations to 0.5.0 with the core: its changesets add the column this needs.
  • Fishing gear catalog. New game data providers FishingBaitProvider, FishingRodProvider and FishingShotProvider (entities fishingbait, fishingrod, fishingshot). Optional: a server without fishing registers none. A host that implements them registers each in META-INF/services and returns null from snapshot() until its data is loaded.
  • An empty game data snapshot now deletes. A registered provider that returns an empty collection (or an empty Optional from GearScoreRulesetProvider) makes the platform delete every row of that entity for the server. Return null while your data is not loaded, never an empty collection.

nx-gs-adapter-api 0.119.0: clanId and allianceId on chat messages

Section titled “nx-gs-adapter-api 0.119.0: clanId and allianceId on chat messages”

Breaking for Java code only. ChatMetadataKeys.CLAN_ID and ChatMetadataKeys.ALLIANCE_ID are removed, with no deprecated copy. ChatMessageEvent now has the fields clanId and allianceId (Long, nullable) on every channel; its constructor takes them as two new trailing parameters, and the builder has clanId(...) and allianceId(...).

  • A host that wrote the clan and alliance ids into the event metadata sets the two fields instead and stops writing those keys: the platform no longer reads them from metadata.
  • The fields hold the speaker’s clan and alliance at the moment of speaking. A message without a legal sender carries the addressee: on CLAN its clan and alliance, on ALLIANCE only the alliance. No clan or alliance means null.
  • Code that calls the ChatMessageEvent constructor directly passes null for both new parameters, or the ids.

nx-gs-adapter-api 0.119.0, nx-gs-adapter-core 0.50.0: revive of a dead character

Section titled “nx-gs-adapter-api 0.119.0, nx-gs-adapter-core 0.50.0: revive of a dead character”

Additive: a host that does not handle the new command needs no change. Until it does, the platform offers no revive for the server (the published app settings carry no revive group), and an older host answers the command as any unknown one.

  • New ReviveCharacterCommand / ReviveCharacterResult: gets a dead character up at a restart point (TOWN), with the client connected or already closed. A host that wants the feature registers a handler for it and refuses with a ReviveRefusalReason code where the revive is not possible.
  • New AppRules members reviveEnabled(), reviveTownMode() and reviveTownSpotPools(), and the keys app.revive.enabled (default true), app.revive.town.mode (NATIVE by default, or SPOT_POOL) and app.revive.town.spot-pool, see configuration. A server file without them keeps working. AppSettings gains a revive field; its public constructor takes it as a new parameter, so code that builds AppSettings itself passes null or a ReviveSettings.

nx-gs-adapter-api 0.117.1, nx-gs-adapter-core 0.49.1: LifeStoneGrade.JEWELRY, GameSettingsProvider

Section titled “nx-gs-adapter-api 0.117.1, nx-gs-adapter-core 0.49.1: LifeStoneGrade.JEWELRY, GameSettingsProvider”

Breaking for Java code only. The LifeStoneGrade constant ACCESSORY is now JEWELRY (neck, ear and finger slots); no deprecated copy is kept. A host that builds life stone data replaces the constant, nothing else; the grade is sent by name, so a host not yet updated keeps sending ACCESSORY until it is rebuilt.

Additive: the optional GameSettingsProvider (see api) publishes the engine configuration (enchant caps, attribute limits, mail fees, the offline private store fee). A host without it keeps working and publishes no game settings; a host that implements it registers it in META-INF/services and returns null from gameSettings(...) until its configuration is loaded. Two registered implementations fail the module.

nx-gs-adapter-api 0.116.0, nx-gs-gd-sync-core 0.15.0: StatModifier, augmentation catalog

Section titled “nx-gs-adapter-api 0.116.0, nx-gs-gd-sync-core 0.15.0: StatModifier, augmentation catalog”

Breaking for Java code only. SkillStatModifier (package app.l2nx.gs.adapter.api.kafka.sync.gd.skill.effect) is now StatModifier in app.l2nx.gs.adapter.api.kafka.sync.gd: skills and item options share it. Fields, getters, builder and JSON are unchanged, and no deprecated copy is kept. A host that builds skill data replaces the import and the class name, nothing else.

Additive: three new game data providers, ItemOptionProvider, AugmentationTargetGroupProvider and LifeStoneProvider (entities itemoption, augmentationtargetgroup, lifestone). A host without them keeps working; a host that implements them registers each in META-INF/services like the other providers and returns null from snapshot() until its data is loaded. Update nx-gs-gd-sync-core together with the api, since it is the module that publishes them.

nx-gs-adapter-api 0.115.0, nx-gs-adapter-core 0.48.0: open sessions of a type, APP_INTERRUPTED

Section titled “nx-gs-adapter-api 0.115.0, nx-gs-adapter-core 0.48.0: open sessions of a type, APP_INTERRUPTED”

Additive: a host that uses neither addition needs no change.

  • New NxAppSessions.findOpen(ActivityType activity): every session of that type the adapter holds (running, awaiting the restore after a restart, a start in flight, an end still being written), ordered by startedAt, then id. It reads memory only, so any thread may call it; without a running session engine it answers an empty list. Call it right before hostReady() to count the sessions awaiting the restore against your per-account and per-device limits and to keep their spots (OfflineSession.getSpotPool(), getSpotId()) out of your free-spot pick.
  • New end reason LogoutReason.APP_INTERRUPTED, accepted by NxAppSessions.onActionEnded: the game stopped the placed character’s action (it was teleported, the tool or consumable the action needs vanished, the action failed to restart). Use it instead of KICKED (a staff kick) or APP_STOPPED (the player’s stop). Send it only once the platform accepts it: a new reason reaches the adapter first, then the platform, then the host.

nx-gs-adapter-api 0.114.0, nx-gs-adapter-core 0.47.0: default title of a placed character

Section titled “nx-gs-adapter-api 0.114.0, nx-gs-adapter-core 0.47.0: default title of a placed character”

app.phantom.title.text now defaults to l2nx.app instead of no title. A server file without the key shows l2nx.app over characters the app placed in the world. To keep the character’s own title, set the key to an empty value (app.phantom.title.text=). No code change for hosts.

nx-gs-adapter-api 0.112.0, nx-gs-adapter-core 0.46.0, nx-gs-db-migrations 0.4.0: private-store start cooldown

Section titled “nx-gs-adapter-api 0.112.0, nx-gs-adapter-core 0.46.0, nx-gs-db-migrations 0.4.0: private-store start cooldown”

What changed. A private store started from the app can no longer be restarted right away to dump prices: a new start of a character waits a few minutes after the real start of that character’s previous app private store. The adapter carries the rule and the data; the host enforces it.

  • New key app.private-store.start-cooldown-minutes of l2nx-app.properties (default 5, 0 turns the rule off, 0-1440; see configuration) and its typed value AppRules.privateStoreStartCooldownMinutes().
  • New NxAppSessions.findLastStartedByCharacter(int characterId, ActivityType activity): the character’s session of that type with the latest real start, open or closed. Same rules as the master-account read: starts that never really happened do not count.
  • Renamed: NxAppSessions.findLastStarted(UUID, ActivityType) is now findLastStartedByMasterAccount(UUID, ActivityType); no deprecated copy is kept.
  • Both reads complete the future exceptionally on a database failure and also while the adapter’s session engine does not run (still booting, disabled, or not started). Before this release the master-account read answered an empty Optional in that case.
  • New refusal token PrivateStoreReason.COOLDOWN (INVALID_STATE, with retryAt).
  • The settings snapshot AppSettings: new privateStoreStartCooldownMinutes, and startCooldownMinutes is renamed to fishingStartCooldownMinutes (getter and builder method too). The constructor takes the new field, so code that builds AppSettings (tests included) uses the builder.
  • New changeset 0010 of nx-gs-db-migrations: an index of the adapter’s session table that serves the per-character read. The adapter applies it at start, like the earlier changesets.

What to do.

  1. In every private-store start from the app (sell, package sell, buy), when NxAppRules.current().privateStoreStartCooldownMinutes() is above 0, read findLastStartedByCharacter(characterId, ActivityType.OFFLINE_PRIVATE_STORE) and refuse the start with COOLDOWN and retryAt = its startedAt plus the cooldown while that moment is still ahead. Wait for the future on your command thread, never on the adapter IO pool.
  2. A failed read refuses the start as temporarily unavailable, never as “no previous store”.
  3. Never apply the cooldown to a stop: closing a store is always allowed.
  4. Rename calls of findLastStarted(masterAccountId, activity) to findLastStartedByMasterAccount, and uses of AppSettings startCooldownMinutes to fishingStartCooldownMinutes.
  5. Upgrade nx-gs-db-migrator-liquibase5 to 0.2.2: it brings nx-gs-db-migrations 0.4.0 (changeset 0010).

nx-gs-adapter-api 0.111.0, nx-gs-adapter-core 0.45.0: app settings file

Section titled “nx-gs-adapter-api 0.111.0, nx-gs-adapter-core 0.45.0: app settings file”

What changed. The adapter now owns the per-server app settings: the file l2nx-app.properties (see configuration), validated as a whole and reloaded without a restart. The host no longer supplies or publishes settings. A host that implements OfflineSessionHost or calls NxAppSessions must adapt in the same release that upgrades the api and the core:

  • OfflineSessionHost.settings() is removed; delete the implementation.
  • OfflineSessionSettings is removed. Its values are now keys of the file: return threshold, retention days, restore batch size and pause.
  • NxAppSessions.publishSettings is removed: the adapter publishes the settings itself. Stop calling it and drop the host code that built AppSettings.
  • OfflineSession has a new nullable returnThresholdMs, fixed at the session’s start; the returnToOrigin flag of removeFromWorld is computed from it.
  • New: NxAppSessions.findOpenByMasterAccount and findLastStarted, and NxAppRules (NxAdapter.appRules(), ConnectContext.appRules(), CommandContext.appRules()) for the typed rules.

What to do.

  1. Remove settings() from your OfflineSessionHost and the calls to publishSettings.
  2. Create l2nx-app.properties next to l2nx.properties, starting from the reference file, and move your former settings values into it. Every session type is off by default: enable the ones the server offers.
  3. Read the rules (cooldowns, durations, windows, spot pool) from NxAppRules.current() once per check instead of from your own configuration.
  4. If you build OfflineSession yourself (tests included), pass the new returnThresholdMs.

The schema changes (a nullable column and an index on the adapter’s session table) are applied by the adapter’s migrator at start.

nx-gs-adapter-api 0.110.0: private-store contract

Section titled “nx-gs-adapter-api 0.110.0: private-store contract”

What changed. The private-store commands and events were reshaped. A host that handles StartPrivateStore* commands, refuses a store start or purchase, or reads purchase events must adapt in the same release that upgrades the api:

  • A new StartPrivateStoreBuyCommand opens a buy store; its lines are WantedLine (itemTemplateId, count, priceAdena). StartPrivateStoreSellCommand and StartPrivateStorePackageSellCommand now extend the new StartPrivateStoreSellingCommand, which carries getLines() and getSellerFeePercent(); the common base StartPrivateStoreCommand no longer has them.
  • DroppedLine takes a required lineIndex (0-based index into the command’s lines), and its itemId is nullable.
  • PrivateStoreFees, MAX_FEE_PERCENT and BuyFromPrivateStoreCommand.MAX_LINES are removed: limits are the platform’s decision. A fee percent is validated only as not negative (seller fee also at most 100); refuse an oversized purchase with TOO_MANY_LINES.
  • The WellKnown* private-store constant classes are replaced by the enums PrivateStoreReason, DroppedLineReason and PrivateStoreMetadataKey. SellerFee is now PrivateStoreFee, and PrivateStoreMetadata reads the seller and buyer fee from a purchase event’s metadata.
  • The purchase metadata source is now a SessionSource name, CLIENT or APP (it was in_game or remote).
  • New lenient parsers return null for an unknown token: SessionSource.fromToken and the fromToken of the new enums.

What to do.

  1. Replace the removed constants and classes with the enums and types above; take the lines of a sell command from StartPrivateStoreSellingCommand.
  2. Fill lineIndex in every DroppedLine you report.
  3. Send CLIENT or APP as the purchase source, and read it with SessionSource.fromToken.
  4. Handle StartPrivateStoreBuyCommand to support buy stores.

nx-gs-adapter-core 0.40.0: adapter-owned schema migrations

Section titled “nx-gs-adapter-core 0.40.0: adapter-owned schema migrations”

What changed. The adapter now creates and updates its own l2nx_* tables in the game database itself, before any module starts. A game server without a schema migrator on the classpath goes FAILED at start.

What to do on a game server (a login server is not affected):

  1. Run the server on Java 17 or newer.
  2. Add nx-gs-db-migrator-liquibase5 and org.liquibase:liquibase-core 5.x to the runtime classpath, in the same release that upgrades the core.
  3. Make sure exactly one JdbcConnectionSource is registered, and that its database user has write and DDL rights on the l2nx_* tables and CREATE in the schema.
  4. Remove the host’s own DDL for l2nx_* tables, if it had any: the adapter owns them now. An existing l2nx_command_receipts table is adopted as is, with its data.
  5. If the host reads l2nx_* tables during its boot before NxAdapter.start(), call NxAdapter.migrateSchema() first.