Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ set(GCC_MINIMUM_VERSION "7.5")
set(CLANG_MINIMUM_VERSION "7.0.1")

# You should not need to edit these manually, use util/bump_version.sh
set(VERSION_MAJOR 5)
set(VERSION_MINOR 17)
set(VERSION_MAJOR 0)
set(VERSION_MINOR 1)
set(VERSION_PATCH 0)
set(VERSION_EXTRA "" CACHE STRING "Stuff to append to version string")

# Change to false for releases
set(DEVELOPMENT_BUILD TRUE)
# OpenClassCraft's first public release.
set(DEVELOPMENT_BUILD FALSE)

set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
if(VERSION_EXTRA)
Expand Down
79 changes: 79 additions & 0 deletions PROJECT_CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# OpenClassCraft Delivery Checklist

Status: `[x]` delivered and verified, `[-] partly implemented or needs verification`, `[ ] planned`.

## Community Edition

### Core Game
- [-] Windows release build and launcher
- [ ] Linux release build and test on Ubuntu
- [ ] macOS release build and test
- [-] LAN host and join workflow
- [-] Educator and student roles
- [-] Local worlds, backups, and basic permissions
- [-] OpenClassCraft branding, textures, skins, menus, and accessibility settings
- [-] Inventory, building, breaking, tools, crafting, day/night, weather, and sound reliability pass

### Learning Tools
- [-] Robot programming: movement, conditions, loops, variables, sensors, and wait
- [-] Class Guides: dialogue bubbles, instructions, and reference links
- [-] Editable chalkboards
- [-] Checkpoint flags
- [-] Chemistry Lab recipes and spawned results
- [-] Creator Lab visual editor
- [ ] Starter lesson worlds: coding, chemistry, science, and environmental studies

### Educator Workflow
- [-] Educator mode selection
- [-] Place and edit Guides, boards, flags, robots, and chemistry labs
- [-] Save and load local lesson worlds
- [ ] Student-safe inventory and per-world build permissions review
- [-] Local progress checkpoints

### Open Source and Release
- [-] Luanti-derived notices and source availability workflow
- [-] Community documentation and installer scripts
- [ ] Community lesson examples and contributor guide refresh

## Teacher Console

### Classroom Management
- [-] School and teacher profile
- [-] Student records and manual groups
- [-] CSV class-list import
- [-] Assign groups to worlds and lessons
- [ ] Student-friendly LAN join-code workflow
- [ ] Educator permission controls

### Lesson Workflow
- [-] Create, edit, delete, save, and restore lesson plans
- [ ] Lesson versions
- [ ] Starter-world assignment
- [ ] Attach NPC dialogue, boards, flags, chemistry, and activity settings
- [ ] Lock/unlock tools and blocks by lesson stage
- [ ] Reset a lesson world

### Assessment
- [-] Checkpoint progress and teacher notes
- [ ] Robot program completion, chemistry results, and build submissions
- [ ] Rubrics
- [ ] Student portfolios and world snapshots
- [-] CSV reports
- [ ] PDF reports and dashboards by class/group/student

### School Operations
- [-] Offline-first local storage and JSON backups
- [ ] Encrypted local storage and encrypted backups
- [ ] Update manager
- [-] Teacher action audit log
- [ ] Opt-in cloud sync
- [ ] Support, training, and curriculum-pack delivery workflow

## Integration and Quality Gates
- [-] Document a local LAN integration protocol between the game and Teacher Console
- [ ] Authenticate educator-issued session data
- [-] Synchronize only approved progress and lesson events
- [ ] Automated tests for saves, imports, permissions, and reports
- [ ] Crash reports with explicit consent
- [ ] Performance profiling for classroom-size worlds
- [ ] Run a pilot with 1-3 schools and record feedback
2 changes: 1 addition & 1 deletion creator-app/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
<body>
<header class="topbar"><div class="brand"><span class="brand-mark">{ }</span><span>OpenClassCraft Creator</span></div><label class="project-name">Project <input id="projectName" value="Classroom Adventure" maxlength="40" /></label><div class="top-actions"><button id="resetButton" class="icon-button">New</button><button id="saveButton" class="primary-button">Export Mod</button></div></header>
<main class="layout"><section class="workspace-panel"><div class="panel-heading"><h1>Build behavior</h1><span id="saveState">Ready</span></div><div id="blocklyDiv"></div></section><aside class="inspector"><section class="preview-panel"><div class="panel-heading"><h2>Live preview</h2></div><div class="preview-stage"><div id="previewBlock" class="preview-block garden"><span id="previewLabel">Classroom Adventure</span></div></div><label>Block name <input id="blockName" value="Classroom Adventure Block" maxlength="48" /></label><label>Style <select id="blockStyle"><option value="garden">Garden</option><option value="stone">Stone</option><option value="wood">Wood</option><option value="glass">Glass</option></select></label><label>Category <select id="category"><option>Classroom</option><option>Science</option><option>Activity</option></select></label></section><section class="help-panel"><h2>Safe creator blocks</h2><p>Use the colored blocks to decide what happens when students interact with your creation.</p><p>Your project exports as a small world mod. It contains only the actions available in this editor.</p></section></aside></main>
<xml id="toolbox" style="display:none"><category name="Events" colour="#18a999"><block type="occ_when_placed"></block><block type="occ_when_player_near"></block></category><category name="Actions" colour="#ff8b3d"><block type="occ_say"></block><block type="occ_give_item"></block><block type="occ_change_block"></block><block type="occ_wait"></block></category><category name="Logic" colour="#7b61c9"><block type="controls_if"></block><block type="logic_compare"></block><block type="occ_sensor_nearby"></block></category><category name="Loops" colour="#dd5f9b"><block type="controls_repeat_ext"></block></category><category name="Variables" colour="#4e95d9" custom="VARIABLE"></category></xml>
<xml id="toolbox" style="display:none"><category name="Events" colour="#18a999"><block type="occ_when_placed"></block><block type="occ_when_player_near"></block></category><category name="Actions" colour="#ff8b3d"><block type="occ_say"></block><block type="occ_give_item"></block><block type="occ_change_block"></block><block type="occ_wait"></block><block type="occ_change_variable"></block></category><category name="Logic" colour="#7b61c9"><block type="controls_if"></block><block type="logic_compare"></block><block type="occ_sensor_nearby"></block></category><category name="Loops" colour="#dd5f9b"><block type="controls_repeat_ext"></block></category><category name="Variables" colour="#4e95d9" custom="VARIABLE"></category></xml>
<script src="renderer.js"></script>
</body></html>
20 changes: 19 additions & 1 deletion creator-app/app/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Blockly.defineBlocksWithJsonArray([
{ type: "occ_give_item", message0: "give item %1", args0: [{ type: "field_dropdown", name: "ITEM", options: [["chalkboard", "openclasscraft_classroom:chalkboard"], ["robot spawner", "openclasscraft_classroom:robot_spawner"], ["science lab", "openclasscraft_classroom:chemistry_lab"]] }], previousStatement: null, nextStatement: null, colour: 25 },
{ type: "occ_change_block", message0: "change this block to %1", args0: [{ type: "field_dropdown", name: "BLOCK", options: [["garden", "default:dirt_with_grass"], ["stone", "default:stone"], ["water", "default:water_source"]] }], previousStatement: null, nextStatement: null, colour: 25 },
{ type: "occ_wait", message0: "wait %1 seconds", args0: [{ type: "field_number", name: "SECONDS", value: 1, min: 0, max: 60, precision: 0.5 }], previousStatement: null, nextStatement: null, colour: 25 },
{ type: "occ_change_variable", message0: "change variable %1 by %2", args0: [{ type: "field_variable", name: "VAR", variable: "score" }, { type: "field_number", name: "CHANGE", value: 1, min: -1000, max: 1000 }], previousStatement: null, nextStatement: null, colour: 210 },
{ type: "occ_sensor_nearby", message0: "a player is nearby", output: "Boolean", colour: 280 },
]);

Expand All @@ -23,6 +24,17 @@ function escapeLua(value) { return String(value).replace(/\\/g, "\\\\").replace(
function quote(value) { return `\"${escapeLua(value)}\"`; }
function indent(code) { return code.split("\n").filter(Boolean).map((line) => ` ${line}\n`).join(""); }
function identifier(value) { return String(value).toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 32) || "classroom_block"; }
function variableKey(block) {
const model = workspace.getVariableById(block.getFieldValue("VAR"));
return identifier(model ? model.name : "value");
}
function valueCode(block) {
if (!block) return "0";
if (block.type === "math_number") return String(Number(block.getFieldValue("NUM")) || 0);
if (block.type === "text") return quote(block.getFieldValue("TEXT"));
if (block.type === "variables_get") return `(tonumber(storage:get_string(name .. \":${variableKey(block)}\")) or 0)`;
return "0";
}

function statementCode(block) {
let code = "";
Expand All @@ -31,6 +43,12 @@ function statementCode(block) {
else if (current.type === "occ_give_item") code += `player:get_inventory():add_item(\"main\", ${quote(current.getFieldValue("ITEM"))})\n`;
else if (current.type === "occ_change_block") code += `minetest.set_node(pos, {name = ${quote(current.getFieldValue("BLOCK"))}})\n`;
else if (current.type === "occ_wait") code += `minetest.after(${Number(current.getFieldValue("SECONDS")) || 0}, function() end)\n`;
else if (current.type === "variables_set") code += `storage:set_string(name .. \":${variableKey(current)}\", tostring(${valueCode(current.getInputTargetBlock("VALUE"))}))\n`;
else if (current.type === "occ_change_variable") {
const key = variableKey(current);
const change = Number(current.getFieldValue("CHANGE")) || 0;
code += `storage:set_string(name .. \":${key}\", tostring((tonumber(storage:get_string(name .. \":${key}\")) or 0) + ${change}))\n`;
}
else if (current.type === "controls_repeat_ext") {
const inner = statementCode(current.getInputTargetBlock("DO"));
code += `for _ = 1, ${Math.max(1, Number(current.getFieldValue("TIMES")) || 1)} do\n${indent(inner)}end\n`;
Expand All @@ -55,7 +73,7 @@ function generateLua() {
}
const interaction = onPlace ? `\non_rightclick = function(pos, node, clicker)\n local player = clicker\n local name = player:get_player_name()\n${indent(onPlace)}end,\n` : "";
const nearBehavior = nearby ? `\nminetest.register_abm({\n label = ${quote(`${description} nearby action`)},\n nodenames = {\"openclasscraft_${id}:block\"},\n interval = 1,\n chance = 1,\n action = function(pos)\n for _, object in ipairs(minetest.get_objects_inside_radius(pos, 3)) do\n if object:is_player() then\n local player = object\n local name = player:get_player_name()\n${indent(nearby)} break\n end\n end\n end,\n})\n` : "";
return `-- Generated by OpenClassCraft Creator.\n-- This file only contains actions selected in the visual editor.\nminetest.register_node(\"openclasscraft_${id}:block\", {\n description = ${quote(description)},\n tiles = {${quote(texture)}},\n groups = {crumbly = 2, classroom = 1},\n${interaction}})\n${nearBehavior}`;
return `-- Generated by OpenClassCraft Creator.\n-- This file only contains actions selected in the visual editor.\nlocal storage = minetest.get_mod_storage()\n\nminetest.register_node(\"openclasscraft_${id}:block\", {\n description = ${quote(description)},\n tiles = {${quote(texture)}},\n groups = {crumbly = 2, classroom = 1},\n${interaction}})\n${nearBehavior}`;
}

function refreshPreview() {
Expand Down
2 changes: 1 addition & 1 deletion games/luanti_edu/mods/creative/inventory.lua
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ function creative.register_tab(name, title, items)
"label[5.8,4.15;" .. minetest.colorize("#FFFF00", tostring(pagenum)) .. " / " .. tostring(pagemax) .. "]" ..
[[
image[4.08,4.2;0.8,0.8;creative_trash_icon.png]
listcolors[#00000069;#5A5A5A;#141318;#30434C;#FFF]
listcolors[#10141855;#5A5A5AAA;#14131899;#30434C;#FFF]
list[detached:trash;main;4.02,4.1;1,1;]
listring[]
image_button[5,4.05;0.8,0.8;creative_prev_icon.png;creative_prev;]
Expand Down
4 changes: 2 additions & 2 deletions games/luanti_edu/mods/default/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ local function default_build_formspec_prepend(player)
local colorblind = default_setting_enabled("openclasscraft_colorblind_support")
local large_ui = default_setting_enabled("openclasscraft_large_ui")

local bgcolor = high_contrast and "#000000F4" or "#080808BB"
local normal = high_contrast and "#111111F8" or "#00000069"
local bgcolor = high_contrast and "#000000F4" or "#08080870"
local normal = high_contrast and "#111111F8" or "#00000048"
local hover = high_contrast and "#2D3748" or "#5A5A5A"
local border = high_contrast and "#FFFFFF" or "#141318"
local selected = colorblind and "#0072B2" or "#30434C"
Expand Down
Binary file modified games/luanti_edu/mods/default/textures/default_chest_front.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified games/luanti_edu/mods/default/textures/default_chest_inside.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified games/luanti_edu/mods/default/textures/default_chest_lock.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified games/luanti_edu/mods/default/textures/default_chest_side.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified games/luanti_edu/mods/default/textures/default_chest_top.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified games/luanti_edu/mods/default/textures/gui_formbg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified games/luanti_edu/mods/default/textures/gui_hb_bg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified games/luanti_edu/mods/default/textures/gui_hotbar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified games/luanti_edu/mods/default/textures/gui_hotbar_selected.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion games/luanti_edu/mods/default/tools.lua
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,5 @@ local hidden_tools = {

for _, name in ipairs(hidden_tools) do
minetest.clear_craft({output = name})
minetest.clear_craft({recipe = name})
minetest.unregister_item(name)
end
1 change: 1 addition & 0 deletions games/luanti_edu/mods/give_initial_stuff/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ local STARTER_ITEMS = {
"openclasscraft_classroom:chlorine_atom 8",
"openclasscraft_classroom:lesson_planner",
"openclasscraft_classroom:lesson_marker 3",
"openclasscraft_creator:world_edit_wand",

-- All Coding Blocks (5 of each)
"luanti_coding:start",
Expand Down
2 changes: 1 addition & 1 deletion games/luanti_edu/mods/give_initial_stuff/mod.conf
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
name = give_initial_stuff
description = Gives players diamond tools and coding blocks when they first join
depends = default, luanti_coding, luanti_robot, openclasscraft_classroom
depends = default, luanti_coding, luanti_robot, openclasscraft_classroom, openclasscraft_creator
127 changes: 125 additions & 2 deletions games/luanti_edu/mods/openclasscraft_classroom/init.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
local S = minetest.get_translator("openclasscraft_classroom")
-- The bridge is opt-in: it can only reach a token-protected server on the
-- teacher's own computer when this mod is explicitly granted HTTP access.
local teacher_bridge_http = minetest.request_http_api and minetest.request_http_api()

local NPC_GRAVITY = -9.81
local NPC_LOOK_RADIUS = 6
Expand Down Expand Up @@ -174,6 +177,7 @@ local lesson_task_types = {
teacher = "Teacher check",
}
local lesson_type_order = {"chalkboard", "guide", "marker", "water", "teacher"}
local teacher_bridge_report_progress = function() end

local function get_lesson()
local data = lesson_storage:get_string("active_lesson")
Expand Down Expand Up @@ -224,6 +228,7 @@ local function lesson_try_advance(player, source)

progress = progress + 1
set_lesson_progress(player, lesson, progress)
teacher_bridge_report_progress(player, lesson, progress)
if progress >= #lesson.tasks then
minetest.chat_send_player(player:get_player_name(),
"[OpenClassCraft] Lesson complete: " .. lesson.title)
Expand Down Expand Up @@ -653,6 +658,11 @@ local function register_classroom_board(name, description, surface_texture)
meta:set_string("board_name", description)
meta:set_string("infotext", description)
update_board_surface(pos)
minetest.get_node_timer(pos):start(0.2)
end,
on_timer = function(pos)
update_board_surface(pos)
return false
end,
after_place_node = function(pos, placer)
if placer and placer:is_player() then
Expand Down Expand Up @@ -687,7 +697,7 @@ end
register_classroom_board("openclasscraft_classroom:chalkboard", "Large Blackboard",
"default_obsidian.png^[colorize:#111820:210")
register_classroom_board("openclasscraft_classroom:whiteboard", "Large Whiteboard",
"default_paper.png^[colorize:#F4F2EA:100")
"openclasscraft_classroom_whiteboard.png")

minetest.register_lbm({
name = "openclasscraft_classroom:restore_board_labels",
Expand Down Expand Up @@ -911,7 +921,8 @@ minetest.register_node("openclasscraft_classroom:lesson_marker", {
mesh = "openclasscraft_classroom_checkpoint_flag.obj",
-- Mesh material slots are ordered by material name: cloth, then stand.
tiles = {"openclasscraft_classroom_flag_red.png", "default_steel_block.png"},
inventory_image = "openclasscraft_classroom_flag_red.png",
inventory_image = "openclasscraft_classroom_checkpoint_flag.png",
wield_image = "openclasscraft_classroom_checkpoint_flag.png",
paramtype2 = "facedir",
groups = {cracky = 2, oddly_breakable_by_hand = 2},
selection_box = {
Expand Down Expand Up @@ -1090,3 +1101,115 @@ minetest.register_on_player_receive_fields(function(player, formname, fields)
end
end
end)

local function bridge_apply_lesson(payload)
if type(payload) ~= "table" or not payload.active or type(payload.lesson) ~= "table" then
return false, "No active Teacher Console lesson."
end
if type(payload.lesson.title) ~= "string" or type(payload.lesson.tasks) ~= "table" then
return false, "Teacher Console sent an invalid lesson."
end

local tasks = {}
for _, task in ipairs(payload.lesson.tasks) do
if type(task) == "table" and type(task.text) == "string" and task.text ~= "" then
tasks[#tasks + 1] = {kind = lesson_task_types[task.kind] and task.kind or "teacher", text = task.text}
end
end
if #tasks == 0 then
return false, "The selected lesson has no checkpoints."
end

local lesson = get_lesson()
lesson.owner = "Teacher Console"
lesson.title = payload.lesson.title
lesson.goal = type(payload.lesson.goal) == "string" and payload.lesson.goal or ""
lesson.tasks = tasks
lesson.revision = lesson.revision + 1
save_lesson(lesson)
lesson_storage:set_string("teacher_bridge_version", tostring(payload.updatedAt or ""))
return true, "Imported " .. lesson.title .. " from Teacher Console."
end

local function bridge_fetch_lesson(notify_player)
local url = minetest.settings:get("openclasscraft_teacher_bridge_url") or ""
local token = minetest.settings:get("openclasscraft_teacher_bridge_token") or ""
if not teacher_bridge_http then
return false, "Teacher bridge needs secure.http_mods = openclasscraft_classroom."
end
if url == "" or token == "" then
return false, "Teacher bridge settings are not configured."
end

teacher_bridge_http.fetch({
url = url,
timeout = 3,
quiet = true,
extra_headers = {"X-OpenClassCraft-Token: " .. token},
}, function(result)
if not result.succeeded or result.code ~= 200 then
if notify_player then
minetest.chat_send_player(notify_player, "[OpenClassCraft] Teacher Console is unavailable.")
end
return
end
local payload = minetest.parse_json(result.data)
local imported, message = bridge_apply_lesson(payload)
if notify_player then
minetest.chat_send_player(notify_player, "[OpenClassCraft] " .. message)
elseif imported then
minetest.log("action", "[OpenClassCraft] " .. message)
end
end)
return true
end

teacher_bridge_report_progress = function(player, lesson, progress)
local url = minetest.settings:get("openclasscraft_teacher_events_url") or ""
local token = minetest.settings:get("openclasscraft_teacher_bridge_token") or ""
if not teacher_bridge_http or url == "" or token == "" then
return
end
teacher_bridge_http.fetch({
url = url,
method = "POST",
timeout = 3,
quiet = true,
data = minetest.write_json({
type = "lesson_progress",
playerName = player:get_player_name(),
lessonTitle = lesson.title,
complete = progress,
total = #lesson.tasks,
at = os.time(),
}),
extra_headers = {
"Content-Type: application/json",
"X-OpenClassCraft-Token: " .. token,
},
}, function(result)
if not result.succeeded then
minetest.log("warning", "[OpenClassCraft] Teacher Console progress event was not delivered.")
end
end)
end

minetest.register_chatcommand("occ_teacher_sync", {
params = "",
description = "Import the active local Teacher Console lesson",
privs = {server = true},
func = function(name)
local started, message = bridge_fetch_lesson(name)
return started, message or "Checking Teacher Console..."
end,
})

local teacher_bridge_timer = 0
minetest.register_globalstep(function(dtime)
teacher_bridge_timer = teacher_bridge_timer + dtime
if teacher_bridge_timer < 20 then
return
end
teacher_bridge_timer = 0
bridge_fetch_lesson()
end)
Loading
Loading