NimBlock تسجيل الدخول

قواعد الإضافات

specVersion 1

إضافة NimBlock ليست شيفرة برمجية: إنها ملف JSON يصف قواعد، يقرأه ويُنفّذه محرّك Paper مكتوب مرة واحدة للجميع. هذه الصفحة تحدد بالضبط ما يُسمح لك بكتابته فيه، ولا شيء آخر ممكن.

قاعدة واحدة، ولا شيء غيرها

القواعد كلها تختصر في جملة واحدة: عندما يحدث هذا، وإذا تحققت هذه الشروط، فافعل ذلك. الإضافة قائمة من القواعد، والقاعدة الواحدة تتكون من مُشغّل واحد، ومن صفر إلى 20 شروط، ومن واحد إلى 50 إجراءات.

لا تُولَّد أي شيفرة Java في أي مكان، ولا يوجد ترجمة برمجية: الخادم يحمّل محرّكًا يقرأ المواصفة فحسب. هذه هي الخاصية المهمة لما بعدها، لأن ثمن أي نسخة جديدة من Minecraft يُدفع مرة واحدة في هذا المحرّك، لا في كل إضافة كُتبت به.

شكل المواصفة

{
    "specVersion": 1,
    "name": "ترحيب",
    "rules": [
        {
            "id": "ترحيب",
            "name": "رسالة الترحيب",
            "match": "all",
            "trigger": { "type": "player.join" },
            "conditions": [
                { "type": "player.has_permission", "params": { "permission": "nimblock.vip" } }
            ],
            "actions": [
                { "type": "player.send_message", "params": { "message": "&6أهلاً بك {player}!" } }
            ]
        }
    ]
}

تكون قيمة match إما all (الافتراضية) أو any، ويمكن لأي شرط أن يحمل "not": true. أما enabled: false فتُبقي القاعدة في الملف دون تفعيلها.

القواعد بترميز EBNF

قوائم الرموز الطرفية أدناه مُولَّدة من الكتالوج: إنها حرفيًا المعرّفات التي يعرف المحرّك تنفيذها، وليست نسخًا مكتوبًا يدويًا.

<plugin>       ::= "{" "specVersion" ":" 1 ","
                       [ "id" ":" <slug64> "," ]
                       "name" ":" <label> ","
                       [ "description" ":" <text500> "," ]
                       "rules" ":" "[" <rule> { "," <rule> } "]" "}"      (* 1..200 *)

<rule>         ::= "{" [ "id" ":" <slug32> "," ]
                       "name" ":" <label> ","
                       [ "enabled" ":" <boolean> "," ]                   (* = true *)
                       [ "match" ":" ( "all" | "any" ) "," ]              (* = "all" *)
                       "trigger" ":" <trigger> ","
                       [ "conditions" ":" "[" [ <condition>
                             { "," <condition> } ] "]" "," ]              (* 0..20 *)
                       "actions" ":" "[" <action>
                             { "," <action> } "]" "}"                     (* 1..50 *)

<trigger>      ::= "{" "type" ":" <trigger-id>   [ "," "params" ":" <params> ] "}"
<condition>    ::= "{" "type" ":" <condition-id> [ "," "not" ":" <boolean> ]
                                                 [ "," "params" ":" <params> ] "}"
<action>       ::= "{" "type" ":" <action-id>    [ "," "params" ":" <params> ] "}"

<params>       ::= "{" [ <param-name> ":" <param-value>
                       { "," <param-name> ":" <param-value> } ] "}"
<param-value>  ::= <template> | <identifier> | <integer> | <number>
                 | <boolean> | <choice>

<template>     ::= { <literal> | <variable> | <colour> }                (* 0..2000 *)
<literal>      ::= /[^{}]/ | "{{" | "}}"
<variable>     ::= "{" /[a-zA-Z][a-zA-Z0-9_]*/ "}"
<colour>       ::= "&" ( "0".."9" | "a".."f" | "k".."o" | "r" )

<identifier>   ::= /[A-Za-z][A-Za-z0-9_:.\/-]{0,63}/
<slug32>       ::= /[A-Za-z0-9_-]{1,32}/
<slug64>       ::= /.{0,64}/
<label>        ::= /.{1,64}/
<text500>      ::= /.{0,500}/

<trigger-id>   ::= "player.join"
                 | "player.quit"
                 | "player.chat"
                 | "player.death"
                 | "player.respawn"
                 | "block.break"
                 | "block.place"
                 | "command"
                 | "schedule.repeat"
<condition-id> ::= "player.has_permission"
                 | "player.is_op"
                 | "player.in_world"
                 | "player.health_below"
                 | "text.contains"
                 | "text.equals"
                 | "number.compare"
                 | "chance"
<action-id>    ::= "player.send_message"
                 | "player.send_actionbar"
                 | "player.send_title"
                 | "player.play_sound"
                 | "player.give_item"
                 | "player.give_effect"
                 | "player.heal"
                 | "player.teleport"
                 | "player.kick"
                 | "broadcast"
                 | "server.run_command"
                 | "cancel_event"
                 | "log"
                 | "delay"

ثمة أمران لا يظهران في قواعد الإنتاج هذه، لأنهما يتعلقان بالاتساق لا بالشكل. أي شرط أو إجراء يتطلب لاعبًا لا يُكتب إلا تحت مُشغّل يوفّر لاعبًا: فـ«شفاء اللاعب» لا معنى له تحت «كل N ثانية». كما أن cancel_event لا يُكتب إلا تحت مُشغّل قابل للإلغاء. كلا الأمرين يُرفض عند الكتابة، لا عند التنفيذ.

القوالب

المعاملات من نوع text هي قوالب: يُستبدَل فيها {player} بما وفّره المُشغّل، وتُعطي {{ قوسًا معقوفًا حرفيًا، أما رموز الألوان &a و&l فتُترجَم.

أي متغيّر لا يعرفه المُشغّل هو خطأ، لا نص. كل مُشغّل يُعلن ما يوفّره (انظر القائمة أدناه): وخارج هذه القائمة، لا شيء يمكن قراءته، ويُرفض أي خطأ إملائي عند الكتابة بدلًا من أن ينتهي به المطاف في دردشة اللاعبين. أما معاملات المُشغّل نفسه، فهي ليست قوالب: تُقرأ عند بدء تشغيل الخادم، قبل أن يكون هناك أي شيء لاستبداله.

أسماء Minecraft تُكتب بطريقتين حسب ما تشير إليه. الغرض يُسمّى بأي من الكتابتين (DIAMOND_SWORD أو diamond_sword). أما الصوت أو التأثير فيُسمّى بمفتاحه في Minecraft (entity.player.levelup، speed) لا بثابت Bukkit: المفتاح يُرسَل كما هو إلى العميل، وهو يبقى صالحًا عبر الإصدارات.

ما لا تسمح به القواعد

إنها مغلقة، ويجب قول هذا قبل أي شيء آخر. كل ما يرد في الكتالوج ممكن، وما عداه مستحيل، وسيبقى كذلك: لا توجد أي وسيلة لتنفيذ شيفرة Java عشوائية. هذا ما يجعل الاستوديو فوريًا (لا شيء يُترجَم برمجيًا) وتجعل أي مواصفة آمنة التثبيت مهما كان مصدرها.

الشروط لا تتداخل: هذا قيد مقصود في الإصدار 1، فشجرة منطقية يصعب رسمها بالقوالب، وتكفي all / any / not في معظم الحالات تقريبًا. المخرج الوحيد هو إجراء «تنفيذ أمر كوحدة تحكم»، الذي يملك على ذلك الخادم بالذات صلاحيات وحدة التحكم بالضبط، لا أكثر ولا أقل.

المُشغّلات

ما يُشغّل قاعدة، والمتغيرات التي يوفّرها كل مُشغّل للقوالب.

player.join لاعب ينضم للخادم

اللحظة التي يدخل فيها اللاعب إلى الخادم، بعد تحميل عالمه.

يوفّر: {player} اللاعب الذي ينضم, {world} العالم الذي يصل إليه

player.quit لاعب يغادر الخادم

اللحظة التي يغادر فيها اللاعب الخادم. لا يزال بالإمكان الوصول إليه، لكن أي رسالة تُرسل إليه تضيع.

يوفّر: {player} اللاعب الذي يغادر, {world} العالم الذي يغادره

player.chat لاعب يكتب في الدردشة قابل للإلغاء

قبل أن تصل الرسالة إلى بقية اللاعبين. قابل للإلغاء: عندها لن يراها أحد.

يوفّر: {player} كاتب الرسالة, {world} عالمه, {message} الرسالة المكتوبة

player.death لاعب يموت

عند موت اللاعب، قبل شاشة إعادة الظهور.

يوفّر: {player} اللاعب الذي مات, {world} عالمه, {killer} اللاعب المسؤول، ويكون فارغا إن لم يوجد

player.respawn لاعب يعاود الظهور

عندما يعود اللاعب إلى اللعبة بعد موته.

يوفّر: {player} اللاعب الذي يعاود الظهور, {world} عالم إعادة الظهور

block.break لاعب يكسر كتلة قابل للإلغاء

قبل أن تختفي الكتلة. قابل للإلغاء: تبقى الكتلة في مكانها.

يوفّر: {player} اللاعب الذي يكسرها, {world} عالمه, {block} نوع الكتلة (DIAMOND_ORE), {x} إحداثي X للكتلة, {y} ارتفاع الكتلة, {z} إحداثي Z للكتلة

block.place لاعب يضع كتلة قابل للإلغاء

قبل أن توضع الكتلة. قابل للإلغاء: لا توضع الكتلة.

يوفّر: {player} اللاعب الذي يضعها, {world} عالمه, {block} نوع الكتلة الموضوعة, {x} إحداثي X للكتلة, {y} ارتفاع الكتلة, {z} إحداثي Z للكتلة

command لاعب يكتب أمرا

ينشئ أمرا جديدا على الخادم. ⚠️ الأمر الذي لم يكن اسمه موجودا من قبل لا يظهر إلا بعد إعادة تشغيل الخادم: لا يقبل Paper أسماء أوامر جديدة إلا عند بدء التشغيل. أما محتوى القاعدة فيُعاد تحميله دون إعادة تشغيل.

  • name text اسم الأمر، بدون شرطة مائلة (/)
  • description text اختياري الوصف الذي يظهر في المساعدة
  • permission text اختياري الصلاحية المطلوبة، اتركها فارغة للجميع

يوفّر: {player} اللاعب الذي كتب الأمر, {world} عالمه, {args} ما يأتي بعد الأمر، كما كُتب

schedule.repeat كل N ثانية

يكرر القاعدة طالما الخادم يعمل. لا علاقة له بأي لاعب: يمكن استخدام الإجراءات التي لا تحتاج لاعبا فقط.

  • seconds integer [1..86400] الفاصل الزمني بالثواني

الشروط

ما يُصفّي. من صفر إلى 20 في كل قاعدة، تُجمَع عبر match.

player.has_permission اللاعب لديه الصلاحية يحتاج لاعبًا
  • permission text الصلاحية
player.is_op اللاعب مشغل يحتاج لاعبًا
player.in_world اللاعب في العالم يحتاج لاعبًا
  • world text اسم العالم
player.health_below اللاعب لديه أقل من X قلوب يحتاج لاعبًا

بنقاط الحياة: 20 نقطة تعادل 10 قلوب.

  • value number [0..1024] نقاط الحياة
text.contains نص يحتوي على
  • text text النص المفحوص
  • search text ما يُبحث عنه فيه
  • ignoreCase boolean اختياري, الافتراضي: true تجاهل حالة الأحرف
text.equals نص يساوي
  • text text النص المفحوص
  • value text القيمة المتوقعة
  • ignoreCase boolean اختياري, الافتراضي: true تجاهل حالة الأحرف
number.compare مقارنة رقمين

كلا الطرفين عبارة عن قالب: {y} تُقارن بـ 62 دون كتابة أي كود.

  • left text الطرف الأيسر
  • operator < | <= | == | != | >= | > المقارنة
  • right text الطرف الأيمن
chance عشوائيا، X % من الوقت
  • percent number [0..100] النسبة المئوية

الإجراءات

ما يحدث فعليًا. من واحد إلى 50 في كل قاعدة، تُنفَّذ بالترتيب.

player.send_message إرسال رسالة إلى اللاعب يحتاج لاعبًا
  • message text الرسالة
player.send_actionbar عرض رسالة فوق شريط الإجراءات يحتاج لاعبًا
  • message text الرسالة
player.send_title عرض عنوان بخط كبير يحتاج لاعبًا
  • title text العنوان
  • subtitle text اختياري العنوان الفرعي
  • fadeIn number [0..60] اختياري, الافتراضي: 0.5 مدة الظهور بالثواني
  • stay number [0..600] اختياري, الافتراضي: 3 مدة البقاء بالثواني
  • fadeOut number [0..60] اختياري, الافتراضي: 0.5 مدة الاختفاء بالثواني
player.play_sound تشغيل صوت للاعب يحتاج لاعبًا
  • sound identifier الصوت، كمفتاح Minecraft (entity.player.levelup)
  • volume number [0..10] اختياري, الافتراضي: 1 مستوى الصوت
  • pitch number [0.5..2] اختياري, الافتراضي: 1 حدة الصوت
player.give_item إعطاء عنصر للاعب يحتاج لاعبًا

ما لا يتسع له المخزون يسقط على الأرض عند قدميه.

  • material identifier العنصر (DIAMOND_SWORD)
  • amount integer [1..2304] اختياري, الافتراضي: 1 الكمية
player.give_effect تطبيق تأثير على اللاعب يحتاج لاعبًا
  • effect identifier التأثير، كمفتاح Minecraft (night_vision)
  • seconds integer [1..86400] اختياري, الافتراضي: 10 المدة بالثواني
  • amplifier integer [0..255] اختياري, الافتراضي: 0 المستوى، بدءا من 0
player.heal شفاء اللاعب يحتاج لاعبًا

الصحة كاملة، والجوع مشبع.

player.teleport نقل اللاعب يحتاج لاعبًا
  • x number [-30000000..30000000] الإحداثي X
  • y number [-512..1024] الارتفاع
  • z number [-30000000..30000000] الإحداثي Z
  • world text اختياري العالم، اتركه فارغا لعالم اللاعب نفسه
player.kick طرد اللاعب يحتاج لاعبًا
  • reason text السبب الذي يظهر
broadcast الإعلان إلى الخادم بأكمله
  • message text الرسالة
server.run_command تنفيذ أمر بصفة الكونسول

منفذ الطوارئ في الصياغة: كل ما يستطيع الكونسول فعله. هذه سلطة الكونسول الكاملة على هذا الخادم، فلا تكتب هنا إلا وأنت تعرف ما تفعله.

  • command text الأمر، بدون شرطة مائلة (/)
cancel_event إلغاء ما حدث للتو يحتاج مُشغّلًا قابلًا للإلغاء

لا يتوفر إلا في المحفزات القابلة للإلغاء: فلا تُرسل الرسالة، ولا تُكسر الكتلة.

log الكتابة في كونسول الخادم
  • message text الرسالة
  • level info | warn اختياري, الافتراضي: info المستوى
delay الانتظار

يوقف بقية إجراءات القاعدة مؤقتا. القاعدة التي تنتظر لم يعد بإمكانها إلغاء أي شيء: يجب أن يأتي «الإلغاء» قبلها.

  • seconds number [0.05..3600] الثواني

الملفات المنشورة

يُولَّد JSON Schema من الكتالوج مع كل عملية نشر، وهو المرجع القابل للقراءة الآلية: أي محرر نصوص يعتمده يكمل المعرّفات تلقائيًا ويرفض المعاملات غير المعروفة.

  • plugin-1.schema.json : المخطط، بصيغة JSON Schema draft 2020-12.
  • catalogue-1.json : الكتالوج نفسه، المصدر الذي يُشتق منه كل شيء آخر (المدقّق، والمخطط، وقوالب المحرر، ومحرّك Java).

الكتالوج يحمل التسميات باللغة الفرنسية، وهي اللغة المصدر. أما المعرّفات فلا تُترجَم: فهي ما يُنفّذه المحرّك فعليًا.

ما يحدث عند تغيّر القواعد

إضافة مُشغّل أو شرط أو إجراء جديد لا تُعطّل شيئًا: أي مواصفة كُتبت سابقًا تستمر في العمل. أما التغيير الجذري فيرفع رقم specVersion، وعندها يرفض المحرّك ما لا يفهمه بدلًا من تنفيذه بشكل خاطئ، ويُعلن ذلك في وحدة تحكم الخادم.

تحمل الروابط أعلاه هذا الرقم: سيبقى plugin-1.schema.json يشير دائمًا إلى هذه القواعد بعينها، حتى يوم يوجد فيه إصدار آخر.