#2 عالم، وحفظ، وإعادات تشغيل، وخطأ مختبئ في صيغة الحفظ
· Nick
منذ الإدخال الأول صار للنواة عالم توضع فيه الأشياء، وملفات تحفظه، وطريقة لإثبات أن جولتين متطابقتان. ما زالت لا رسوم ولا شخصيات. معظم هذا الإدخال عن التمديدات الداخلية، وجزء منه عن خطأ تبيّن أنه يستحق أن يُروى.
عالم من المربعات
العالم شبكة من مربعات بحجم 1×1 م على مستويات منفصلة، مرقّمة من الصفر صعودًا. الجدار أو الباب مربع؛ والسلّم سيكون رابطًا خاصًا بين مستويين، لكن السلالم لم تُبنَ بعد. داخليًا، العالم مجموعة من الطبقات المسطحة (الأرضية، والبنية، وأعلام البنية) للخريطة كلها، إضافة إلى طبقة مشتقة لقابلية المشي. تُتابَع الخريطة في قطع بحجم 16×16 مع عدّادات إصدار، كي يستطيع العارض أو البحث عن المسار أن يسأل «هل تغيرت هذه المنطقة؟» دون مقارنة المربعات.
يُختار حجم العالم عند إنشائه. يتم البناء بأوامر تأخذ مستطيلًا: وضع أرضية أو جدار أو باب على مساحة، أو إزالتها. إذا كان جزء من المستطيل محجوبًا، يُبنى الباقي مع ذلك، ويُبلغ الأمر عمّا تخطاه. حاليًا هناك ثلاثة أنواع مدمجة من المربعات: أرضية، وجدار، وباب. من الآن فصاعدًا الخريطة جزء من بصمة العالم ومن ملفات الحفظ. وتُختبر مقابل نموذج مرجعي بطيء وصحيح بوضوح على آلاف الأوامر العشوائية.
الحفظ وإعادات التشغيل
يتشارك ملف الحفظ وإعادة التشغيل حاويةً واحدة. تنقسم إلى أقسام موسومة، لكل منها نوع وإصدار، بحيث يمكن لأجزاء جديدة من العالم (الشخصيات، والاحتياجات، والعلاقات) أن تصبح أقسامًا جديدة لاحقًا دون كسر الملفات القديمة. هناك مجاميع تحقق على الترويسة والمحتوى، والمحتوى مضغوط، ويُفحص كل حجم مقابل حد قبل حجز أي ذاكرة، كي لا يستطيع ملف تالف أو خبيث أن يجعل اللعبة تلتهم كل ذاكرتك. تُخزَّن أنواع المربعات بأسمائها لا بأرقامها، كي يبقى ملف الحفظ صالحًا إن تغيرت قائمة الأنواع.
إعادة التشغيل هي ما أردته منذ البداية: البذرة مع سجل الأوامر. أثناء التسجيل تكتب النواة أيضًا بصمة تحكّم كل 100 نبضة. وعند التشغيل تقارن نتيجة كل أمر وكل بصمة تحكّم على طول الطريق، وتتوقف عند أول نبضة يختلفان فيها. وتتذكر إعادة التشغيل أيضًا أي إصدار من النواة سجّلها، فتُرفض إعادة التشغيل القادمة من إصدار آخر بدل أن تنحرف بصمت.
النواة نفسها لا تلمس الملفات أبدًا: تقرأ وتكتب تدفقات بيانات، واللعبة هي التي تقرر أين تُحفظ الملفات. ويُفرض هذا الحد بآلية الواجهات المحظورة نفسها كما من قبل.
لم يكتمل بعد: أداة سطر أوامر تشغّل ملف إعادة التشغيل، وجانب اللعبة من الحفظ (مجلد للحفظ، والحفظ التلقائي، والكتابة الآمنة).
إثبات تطابق جولتين
يشغّل اختبار الحتمية الآن سيناريو واحدًا في عمليتين منفصلتين: بذرة، وسكربت من أوامر البناء (بعضها غير صالح عمدًا)، ونظام اختبار يسحب من كل تدفق عشوائي. تشغّله كل عملية بثلاث طرق — مباشرة حتى النهاية، وعبر الحفظ والاستعادة، وعبر إعادة تشغيل البايتات المسجلة — وتقارن البصمات في كل نبضة. ثم تقارن العمليتان بصماتهما ببعضهما كل عشر نبضات.
كسرته عمدًا لأتأكد أنه قادر على الفشل. Random بلا بذرة مهرّب إلى النبضة جعل الاختبار أحمر. أما بصمة نصية عشوائية، يبذرها .NET بشكل مختلف في كل عملية، فلم تُلاحظ داخل عملية واحدة، ولم تُكتشف إلا عند مقارنة عمليتين. وهذا هو السبب الكامل لوجود عمليتين.
القاعدة التي دوّنتها أن إصدارًا واحدًا من النواة يجب أن يعطي البصمات نفسها على معالجات x64 وarm64، فإعادة تشغيل سُجّلت على Mac بشريحة Apple يجب أن تعمل على حاسوب Windows. البصمات المرجعية المثبّتة وأمثلة ملفات الحفظ وإعادة التشغيل موجودة لهذا الفحص تحديدًا. تختلف الدوال المثلثية والأسية بالأعداد العشرية بين المنصات، لذا فهي محظورة في كل ما يؤثر في العالم؛ وسأحتاج بديلًا حتميًا في أول مرة يحتاجها نظام ما. هذه الملفات المرجعية لا تتغير إلا عمدًا: إذا انحرفت بصمة، يجب شرح التغيير قبل قبوله.
اختبار أداء يستطيع أن يرفض
يوجد الآن مشغّل بلا واجهة يأخذ سيناريو وبذرة وعددًا من النبضات، ويطبع تقريرًا بصيغة JSON: الوسيط والمئين التاسع والتسعين لزمن النبضة، وعدد الوكلاء، وبصمة العالم، وكم بايتًا حُجز في كل نبضة. السيناريو الأول يبني خريطة 512×512 على 16 مستوى بنحو 52,000 أمر بناء.
يُحفظ خط الأساس لكل جهاز على حدة، لأن أزمنة حاسوب محمول لا تعني شيئًا على حاسوب مكتبي. التراجع هو وسيط أعلى من خط الأساس بأكثر من 10% أو مئين تاسع وتسعون أعلى منه بأكثر من 35%، وفقط إذا كانت الزيادة أكبر من 0.05 مللي ثانية، لأن النبضة شبه الفارغة تقع عند دقة المؤقت. وهناك بوابة واحدة تسري على كل جهاز: صفر بايت محجوز في كل نبضة. ينبغي أن أكون صريحًا بشأن ما يقيسه هذا اليوم. بلا أنظمة ولا شخصيات تستغرق النبضة نحو 35 نانوثانية، فبوابة الزمن نائمة في الغالب حتى تصل الأنظمة الحقيقية الأولى في مَعلم الحركة. بوابة الحجز هي التي تعمل الآن.
الاختبار المتقلب الذي لم يكن متقلبًا
أثناء بناء اختبار الأداء، بدأ اختبار لصيغة الحفظ يفشل في بعض الإصدارات مع أن النواة لم تتغير. يقلب الاختبار كل بايت من إعادة تشغيل مضغوطة على حدة، ويشترط أن يرفض المحمّل كل واحد منها. في أحد الإصدارات قُبل بايت قرب نهاية الملف. وفي الإصدار السابق نجح الاختبار نفسه.
سبب ظهوره واختفائه: تحمل إعادة التشغيل معرّف الإصدار الذي كتبها، فتختلف البايتات المضغوطة من إصدار إلى آخر، ومعها البايت الذي يُقلب. الثغرة كانت موجودة دائمًا؛ الاختبار لم يلاحظها إلا حين وقع النرد على ذلك الوجه.
الثغرة نفسها: كان مجموع التحقق يغطي البيانات بعد فك الضغط، وتبيّن أن فاك الضغط في .NET متسامح. فهو يقبل تدفقًا لا يعلن أبدًا أنه انتهى، ويتجاهل كل ما بعد النهاية. قلب بت حشو في البايت الأخير يعطي تدفقًا مختلفًا لكنه ما زال صالحًا، ويُفك إلى البيانات نفسها، فيبقى مجموع التحقق مطابقًا ويُحمَّل الملف التالف على أنه سليم.
الإصلاح إصدار جديد من الحاوية. صار مجموع التحقق يغطي البايتات كما هي مخزنة في الملف ويُفحص قبل فك الضغط، ويجب أن ينتهي فك الضغط بالضبط حيث تقول الترويسة. ويُخزَّن المحتوى الفارغ بلا ضغط، لأن الضاغط لا يكتب شيئًا لمدخل فارغ. وتستخدم الفحوص الآن تدفقات مضغوطة ثابتة لا تعتمد على الإصدار، إضافة إلى اختبار عشوائي للملفات التالفة. أُعيد توليد ملفات الأمثلة للإصدار الجديد؛ ولم تتغير بصمات العالم.
في أماكن أخرى
الموقع الذي تقرؤه الآن يعرض في صفحته الرئيسية عدّادين للزوار: الزوار الفريدون منذ البداية وخلال آخر 24 ساعة. يُحسبان من سجل صفحات خادم الويب، بلا JavaScript ولا ملفات تعريف ارتباط ولا خدمات خارجية. لا يُخزَّن العنوان إلا كبصمة مملّحة، وتُستبعد الروبوتات حسب هوية المتصفح، وتتأخر الأرقام بضع دقائق. عدة أشخاص خلف شبكة واحدة يُحسبون واحدًا.
التالي
اكتمل المَعلم الأول، هيكل النواة. التالي هو M1، واجهة البناء في Godot: جدران وأبواب وغرف من نموذج العالم، وتقطيع الطوابق، وكاميرا تدور بخطوات 90°، ومكعبات بسيطة بدل الرسوم. لم يبدأ العمل عليه بعد. انظر خريطة الطريق.