Repository navigation
KubeJS Custom Types
English | 中文
Type IDs must have a namespace, e.g. pack:quest_gate. Each kind allows at most 128 types.
Types are registered again every time the atlas is rebuilt, so registrations never pile up. A type object cannot be changed after registration ends.
Validation, test, description and cost callbacks must be pure computations: no giving items, no running commands, no writing player state. Writes through the player helper inside them are rejected, and recursive quotes are forbidden. Reads such as getLevel / hasSkill are fine. Do not call checkLearn / getLearningQuote / learn inside a condition.
SkillAtlasEvents.conditionTypes(event => {
event.register('pack:quest_gate', type => {
type.validate(data => typeof data.flag === 'string' && typeof data.label === 'string')
.test((ctx, data) => ctx.player.persistentData.getBoolean(data.flag))
.describe((ctx, data) => Text.gold(data.label))
})
})
SkillAtlasEvents.definitions(event => {
event.modifyNode('haste', node => {
node.condition('pack:quest_gate', { flag: 'intro_done', label: 'Finish the intro quest' })
})
})-
validate(data => boolean)is optional; by default every data object is accepted. Data is read-only plain JSON. -
test(ctx, data)is required and returns a boolean. -
describe(ctx, data)is required and returns a Component, such as one built withText. - Unknown types, duplicate registrations, invalid data and incomplete types reject the new atlas.
ctx provides player, nodeId, level (the level about to be learned), phase (preview / check / learn / commit), language and skills (the player helper).
Node conditions and next-level conditions must all pass. requireAll does not turn them into "any" conditions. Custom conditions add to native prerequisites, hidden-node rules and choice groups; they never replace them.
SkillAtlasEvents.rewardTypes(event => {
event.register('pack:apple_gift', type => {
type.validate(data => data.count >= 1 && data.count <= 64)
.describe((ctx, data) => Text.green('Apple ×' + data.count))
.onLearn((ctx, data) => ctx.player.give(Item.of('minecraft:apple', data.count)))
})
})
SkillAtlasEvents.definitions(event => {
event.modifyNode('haste', node => {
node.level(1, rank => rank.customReward('pack:apple_gift', { count: 1 }))
})
})describe is required and validate is optional. A reward needs onLearn, or the pair onActivate + onDeactivate, or all three.
| Callback | When |
|---|---|
onLearn(ctx, data) |
Once for the new level after a paid learn; once per new level for admin grants |
onActivate(ctx, data) |
The level becomes the current one; also on login/respawn and rebuilt on definition reload |
onDeactivate(ctx, data) |
Replaced by another level, respec, definition removal/change, logout, server stop |
- Persistent rewards use only the current level's declarations. History is not stacked.
- Repeated refreshes do not activate twice. A new player entity or a new registration generation first deactivates the old instance, then activates the new one.
- Queries, cancelled or failed learns and re-logins never trigger
onLearn. Paying again after a respec, or granting upward again, counts as a new learn, the same as native one-shot command rewards. For "once per lifetime", use your own persistent flag.
Reward ctx provides player, nodeId, level, index (custom reward slot, from 0), reason, key, modifierId, modifierKey, language and skills.
-
keyis a stable resource key. -
modifierIdis a stable UUID for attribute modifiers on 1.20.1. -
modifierKeyis the same key as aResourceLocation(1.21.1) /Identifier(26.1.2) for attribute modifiers there.
All of them are unique per node, level and slot, and do not depend on the player entity instance.
reason is one of learn, grant, login, reset, definition_change, respawn, refresh, logout, stop. onActivate / onDeactivate must be idempotent per stable key; do not hand out items in onActivate.
Errors after the commit are logged. The learn, payment and level are kept, and one-shot rewards are not retried. A failed activation keeps the instance so it can be cleaned up later. Deactivation errors are logged too, so make your revoke logic reliable.
SkillAtlasEvents.learningCost('haste', event => {
if (event.player.persistentData.getBoolean('discount')) {
event.amount = Math.floor(event.amount * 0.8)
}
event.items = [{ item: 'minecraft:emerald', count: 2 }]
})The event inherits player, node, level, source / reason and cancel / deny. You can change:
| Field | Rule |
|---|---|
amount |
Integer 0–1000000000 |
type |
skill_points or experience (actual experience points, not levels) |
items |
Array of delivery objects, at most 16; each count is a positive integer; nbt follows the target version; [] means no items |
Build the whole JavaScript array first and assign it to event.items in one go. The Java list you read may be read-only, so do not rely on push.
baseCost is a read-only snapshot of the original level JSON (cost, optional experience, items, native rewards). The pool must stay the category's pool, and a category without a pool cannot switch to skill_points. Fractions, non-finite or negative numbers, wrong currencies, invalid items and oversized costs reject the quote. An error or cancel means nothing is charged.
- The detail view and tooltips show the server-quoted cost, condition checkmarks and custom reward descriptions. Until a quote arrives they show "querying" and the detail Learn button stays disabled.
- Quotes are bound to player, node, current level, revision and expiry, and carry a one-time token. Each player caches at most 128 nodes.
- The server accepts at most one quote request per 5 ticks. A quote is valid for 100 ticks. The client refreshes about every 60 ticks and drops stale entries after 80 ticks. Only the node being viewed is refreshed.
- Clicking Learn must use a quote that was displayed. Expired, foreign, outdated, replayed or missing quotes are rejected.
- The actual payment re-checks resources and conditions, and re-checks conditions after
beforeLearn. The price is frozen inside the transaction and never silently re-quoted. -
SkillAtlas.player(p).learn(id)creates its own internal quote, so scripts never handle tokens.
Limits: at most 16 delivery items, each with up to 8192 characters of NBT / component data. Cost JSON can be up to 1 Mi characters and a quote payload up to 1048512 bytes. Oversized descriptions reject the quote instead of truncating it.
With any learningCost listener, every node goes through quoting. With only condition or reward declarations, only those nodes do. Plain native learn requests cannot bypass the cost rules of managed nodes.
Acquisition types let other mods' activities award points.
SkillAtlasEvents.acquisitionTypes(event => {
event.register('pack:machine', type => {
type.name('Machine processing').description('Process items in a specific machine')
.unit('count')
.validate(data => typeof data.machine === 'string')
.test((ctx, data) => ctx.payload.machine === data.machine)
})
})
SkillAtlasEvents.definitions(event => {
event.createPool('machine').name('Processing points')
.customAcquisition('pack:machine', { machine: 'press' }, 8, 1, 20)
})Then report from the real server event of the mod you integrate:
SkillAtlas.player(event.player).emitAcquisition('pack:machine', { machine: 'press' }, 1)-
nameis required.descriptiondefaults toname,unitdefaults tocount, andvalidate/testaccept everything by default. -
name/descriptionmay be translation keys. They are shown in the client language when a translation exists, otherwise as-is. Each is at most 512 characters. -
unitiscount,secondsorcentimeters. Custom units always compare the reportedunitsdirectly against thecountthreshold, without multiplying by 20 or 100.unitsis an integer from 0 to 1000000000. - Config
dataand the per-reportpayloadare passed separately. Each is plain JSON of at most 16 KiB, read-only, limited to 32 nesting levels and 8192 validation steps. -
ctxprovidesplayer,skills,language,typeId,poolId,payloadandunits. - One report first filters every pool of that type, then settles each pool. Unknown types or matching errors award nothing, and a matching error happens before any pool is settled.
The integration provides registration and dispatch only. It does not listen to other mods automatically; your pack bridges the actual events. See Server API for results and eligibility.
KubeJS (English)
- Getting Started
- Events
- Server API
- Atlas Builder
- Custom Types and Costs
- Client API
- Reload and Editor
- Examples
KubeJS(中文)