Skip to content

Navigation: Documentation Home โ€บ Server Owner Guide โ€บ Script Intro Related: ่‡ชๅฎšไน‰GUI.md ยท ้…็ฝฎๆ–‡ไปถ.md ยท ็‰ฉๅ“ๆบๅผ•็”จ.md ยท Developer / ่„šๆœฌAPI.md

๐Ÿ“œ Script Intro (Server Owner Perspective) โ€‹

This page teaches you how to extend QinhCoreLib (QCL) logic using JavaScript scripts. You don't need to know Java, and you don't need to build a pluginโ€”just drop a .js file into a directory, and you can give a GUI custom conditions and actions like "block opening if level is too low" or "click to deduct money and give an item".


๐Ÿค” Why You Need Scripts โ€‹

QCL's GUIs and sub-plugin hooks all support "conditions" and "actions". Plain YAML config covers most common needs (permissions, items, coinsโ€ฆ), but it falls short for needs like these:

NeedPlain YAMLScript
"Only allow opening the shop at level โ‰ฅ 30"โŒ Not flexible enoughโœ… canOpen returns a boolean
"Click to buy: deduct 100 coins first then give the item, reject if not enough money"โš ๏ธ Splits into multiple steps, hard to judgeโœ… One function handles it
"Dynamically decide based on the player's PAPI variable"โŒโœ… qcl.placeholder(...)

Scripts are a "logic back door" opened for server owners: use a small snippet of JS to express "conditions" and "actions".


๐Ÿงฉ Engine Notes (Read This First) โ€‹

QCL's script engine is GraalVM JavaScript (GraalJS).

  • The server (Paper / Purpur) needs to be able to load the GraalJS runtime library.
  • If the runtime library is unavailable, script calls return the failure code SCRIPT_UNAVAILABLE directly (it won't crash the server, but all script logic stops working).
  • After startup, watch the console to confirm the script system loaded correctly.

๐Ÿ“ Where Scripts Go โ€‹

NamespaceDisk DirectoryWhose
globalplugins/QinhCoreLib/scripts/global/Server owner's shared scripts
qinhitemsplugins/QinhItems/scripts/Namespace registered by the sub-plugin QinhItems
OthersRegistered by each sub-plugin itselfSub-plugins

๐Ÿ’ก A namespace is a mapping from a name to a directory. When a server owner writes their own scripts, just drop them into scripts/global/, and the namespace when referencing them is global (which can also be omitted, see below). Sub-plugins can register their own namespace and point its script directory at their own plugin folder.


๐Ÿ”— Reference Syntax Explained โ€‹

A script reference is a string in three parts:

namespace:relative-path.js:functionName
PartDescriptionOmission Rule
namespaceDecides which directory to look inOmitted โ†’ defaults to global
relative-path.jsFile path relative to the namespace directory.js can be omitted, appended automatically
functionNameWhich function in the file to callOmitted โ†’ uses config's javascript.default-function (default main)

Example Comparison โ€‹

FormEquivalent Meaning
qinhitems:hooks/craft.js:checkqinhitems namespace โ†’ hooks/craft.js โ†’ call check
global:example.jsglobal โ†’ example.js โ†’ call main (default function name)
example.js:onClickglobal (namespace omitted) โ†’ example.js โ†’ call onClick
exampleglobal โ†’ example.js (.js appended automatically) โ†’ call main

โœ… All three parts can be omitted; the shortest form is just scriptName, which calls main in scripts/global/scriptName.js.


โš™๏ธ Config Options (config.yml) โ€‹

Three script-related options, located in the main config:

KeyDefaultDescription
javascript.enabledtrueMaster switch; turning it off disables all script calls
javascript.default-functionmainDefault function called when the reference omits the function name
javascript.debug.print-stacktracefalseWhen enabled, prints the full stack trace to the console on script errors (a troubleshooting lifesaver)
yaml
javascript:
  enabled: true
  default-function: main        # Called when the function name is omitted
  debug:
    print-stacktrace: false     # Change to true when troubleshooting

๐Ÿ”„ After changing config or adding new scripts, run /qcl reload to rescan the scripts directory.


โœ๏ธ Write Your First Script (Step by Step) โ€‹

Step 1: Create the File โ€‹

Create a new hello.js under plugins/QinhCoreLib/scripts/global/:

javascript
// hello.js โ€”โ€” Note: use English for key names/function names, Chinese only for comments
function main(ctx) {
    // ctx.player() may be null (when not triggered by a player)
    var p = ctx.player();
    qcl.logInfo("hello.js was called, player=" + (p ? p.getName() : "null"));
    return true;   // Returning true means success
}

Step 2: Reload โ€‹

/qcl reload

Step 3: Reference global:hello.js (or the shorthand hello) somewhere to trigger it. โ€‹

Inside a script you can always get two injected objects: qcl (API) and ctx (context). When called by a GUI, the function signature is fixed as function name(ctx){ ... }.


๐Ÿ“ฆ ctx โ€”โ€” The Context Object โ€‹

ctx represents "the scene of this call" (who triggered it, what data it carried).

MethodReturnsDescription
ctx.player()Player or nullThe triggering player; null when not triggered by a playerโ€”always null-check
ctx.pluginName()StringThe name of the plugin that triggered it
ctx.get(key)ValueRead data carried by the context
ctx.set(key, value)โ€”Write context data (for use by later steps)
ctx.vars()Variable collectionGet all context variables

๐Ÿ› ๏ธ qcl โ€”โ€” The API Object โ€‹

qcl is the toolbox QCL exports for scripts to use.

CategoryMethodDescription
Loggingqcl.logInfo(msg) / logWarn(msg) / logError(msg)Print a log to the console (common for debugging)
Bridgingqcl.bridgeStatusNames()List of currently connected external plugin bridge names
Placeholderqcl.placeholder(text)Parse text using PlaceholderAPI (including %...%)
Economy ยท Queryqcl.economyHas(amount, provider?, currency?)Whether the player has enough money, returns a boolean
Economy ยท Withdrawqcl.economyWithdraw(amount, provider?, currency?)Deduct money, returns a boolean
Economy ยท Depositqcl.economyDeposit(amount, provider?, currency?)Give money, returns a boolean
Item ยท Parseqcl.itemParse(ref)Parse an item source reference into an item
Item ยท Giveqcl.itemGive(ref, amount)Give an item to the player, returns a boolean
Scheduling ยท Syncqcl.runSync(task)Throw a task back to the main thread to execute
Scheduling ยท Delayedqcl.runSyncLater(ticks, task)Execute on the main thread after N ticks of delay
Scheduling ยท Waitqcl.runSyncAndWait(task)Execute on the main thread and wait for the result

๐Ÿ’ฐ The economy methods' provider / currency are optional parameters; if not provided, the default economy is used. The item ref is the unified item source reference (see ็‰ฉๅ“ๆบๅผ•็”จ.md).


๐Ÿ” Return Value Rules (Very Important) โ€‹

What a script function "returns" determines whether QCL considers it a success or failure:

Return ValueVerdictNotes
null / undefined / trueโœ… SuccessReturning nothing also counts as success
falseโŒ FailureThe failure message is "script returned false"
String / Numberโœ… Success with valueCarries the value back along with it
{ success: ..., message: ... }Judged by successYou can customize the failure message
javascript
function check(ctx) {
    var p = ctx.player();
    if (!p) return false;
    if (p.getLevel() < 30) {
        return { success: false, message: "&cYour level is below 30!" };
    }
    return true;
}

๐Ÿ–ผ๏ธ Attaching Conditions and Actions in a GUI โ€‹

The most common use for scripts is to wire logic into a GUI's open condition and click action.

Usage 1: Open Condition (view-requirement) โ€‹

yaml
view-requirement:
  type: javascript
  value: global:example.js:canOpen   # Returns false โ†’ don't allow opening

Usage 2: Click Action (click-actions) โ€‹

yaml
click-actions:
  - type: javascript        # or run_script
    value: global:example.js:onClick

๐Ÿงช Complete Example A: Level Gate for Opening a GUI โ€‹

scripts/global/shop_gate.js

javascript
function canOpen(ctx) {
    var p = ctx.player();
    if (!p) return false;
    if (p.getLevel() < 30) {
        return { success: false, message: "&cYou need level 30 to enter the shop" };
    }
    return true;
}

GUI Config (excerpt)

yaml
view-requirement:
  type: javascript
  value: global:shop_gate.js:canOpen

๐Ÿงช Complete Example B: Click to Buy (Deduct Money, Give Item) โ€‹

scripts/global/buy_sword.js

javascript
function buy(ctx) {
    var p = ctx.player();
    if (!p) return { success: false, message: "&cOnly players can buy" };

    // 1) Check whether there's enough money first
    if (!qcl.economyHas(100)) {
        return { success: false, message: "&cNot enough coins, need 100" };
    }
    // 2) Deduct money
    if (!qcl.economyWithdraw(100)) {
        return { success: false, message: "&cWithdrawal failed" };
    }
    // 3) Give the item (item source reference, 1 of it)
    qcl.itemGive("qinhitems-fire_sword", 1);

    return { success: true, message: "&aPurchase successful!" };
}

GUI Config (excerpt)

yaml
click-actions:
  - type: javascript
    value: global:buy_sword.js:buy

Order is critical: check first โ†’ then deduct โ†’ then give. If any step fails, return a failure object to avoid "money deducted but no item given".


๐Ÿ“– Built-in Example Scripts Explained Function by Function โ€‹

QCL ships with two example scripts that you can copy and modify directly:

scripts/global/example.js โ€‹

javascript
function main(ctx) {
    qcl.logInfo("example.js main() was called, player=" + (ctx.player() ? ctx.player().getName() : "null"));
    return true;
}
function canOpen(ctx) {
    var player = ctx.player();
    if (!player) return false;
    return player.getLevel() >= 0;     // An always-true example condition
}
function onClick(ctx) {
    var player = ctx.player();
    if (!player) return false;
    qcl.logInfo("onClick: " + player.getName());
    return true;
}
FunctionPurposeHow to Reference It in a GUI
mainDefault entry point; logs then returns successCalled when the function name is omitted
canOpenOpen condition example (null-check + level check)value: global:example.js:canOpen
onClickClick action example (logging)value: global:example.js:onClick

scripts/global/qcl_status.js โ€‹

Contains the formatStatus function, which appends output lines to the /qcl status detail command. This is a demonstration of "sub-plugin / system-level" usage; server owners generally don't need to touch it.


๐Ÿ”’ Sandbox Restrictions โ€‹

Scripts run in a restricted sandbox. The allowed and forbidden lists:

โœ… AllowedโŒ Forbidden
Calling the exported qcl APICreating threads
Operating on Bukkit's Player objectFile read/write (IO)
Plain JS logicReflecting arbitrary Java classes
Network access

Want to do something outside the sandbox (like reading an external file)? That's beyond the scope of server owner scripts; you should switch to a sub-plugin or dedicated development capabilities, see ่„šๆœฌAPI.md.


๐Ÿš‘ Error Codes and Troubleshooting โ€‹

When a script call fails, QCL gives an error code:

Error CodeMeaningHow to Fix
SCRIPT_UNAVAILABLEGraalJS runtime library unavailableSwitch to Paper/Purpur, confirm GraalJS is loaded
SCRIPT_PARSE_FAILEDThe reference format is written wrongCheck the spelling of namespace:path:functionName
SCRIPT_NOT_FOUNDThe file doesn't existConfirm the .js file is in the right directory and that you ran /qcl reload
SCRIPT_FUNCTION_MISSINGThe function name doesn't existConfirm the file has this function with matching case
SCRIPT_FAILEDThe script threw an exception during executionEnable print-stacktrace to see the stack trace

๐Ÿ› Debugging Tips โ€‹

  1. Enable the stack trace: change javascript.debug.print-stacktrace to true, and the console will print the full stack trace when a script errors, so you can tell which line blew up at a glance.
  2. Add logging: insert qcl.logInfo("got here: " + variable) at key steps to observe the execution path.
  3. Null-check first: almost all bugs come from ctx.player() being null and not being handledโ€”develop the habit of if (!p) return ....
  4. Always reload after changes: only /qcl reload rescans scripts; otherwise your changes won't take effect.
  5. Case-sensitive: namespaces, file names, and function names are all case-sensitive; copy them exactly and don't casually change the case.

โžก๏ธ Continue Reading โ€‹