Compare commits

...

10 Commits

Author SHA1 Message Date
vincent.p.mercier 262983a8b0 feat: deploy autonomous French web edition for Joy 2026-08-21 12:30:00 +02:00
Adrien Ufferte 643ad6a2b5 Merge pull request #337 from Excello-Recherche-Education/sponsor
Add FUNDING.yml
2026-07-07 10:25:25 +02:00
Adrien Ufferte e7c4405a82 Add FUNDING.yml 2026-07-07 10:23:28 +02:00
Adrien Ufferte b6acbdfbed Merge pull request #328 from Excello-Recherche-Education/3.0.1
Merge 3.0.1 into main
2026-06-18 11:55:04 +02:00
Adrien Ufferte a29a5f21f8 build version update 2026-06-11 12:21:51 +02:00
Adrien Ufferte 89b0ad05ec Merge pull request #327 from Excello-Recherche-Education/optimization
Optimization global
2026-06-11 12:03:20 +02:00
Adrien Ufferte b975b5e805 Debug possible corrupted scene file 2026-06-11 11:49:29 +02:00
Adrien Ufferte 30c1a9863f Optimization global 2026-06-11 11:16:03 +02:00
Adrien Ufferte 90ce89f880 Merge pull request #326 from Excello-Recherche-Education/debug
Debug
2026-06-11 10:42:47 +02:00
Adrien Ufferte dadc6f69ef Add missing file uid 2026-06-11 10:32:15 +02:00
23 changed files with 417 additions and 53 deletions
+7
View File
@@ -0,0 +1,7 @@
.git
.github
.godot
dist
addons/godot-sqlite/bin
*.log
.DS_Store
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: [Excello-Recherche-Education] # Up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
# patreon: # Replace with a single Patreon username
# open_collective: # Replace with a single Open Collective username
# ko_fi: # Replace with a single Ko-fi username
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
# liberapay: # Replace with a single Liberapay username
# issuehunt: # Replace with a single IssueHunt username
# lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
# polar: # Replace with a single Polar username
# buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
# thanks_dev: # Replace with a single thanks.dev username
# custom: # Up to 4 custom URLs e.g., [link1, link2]
+66
View File
@@ -0,0 +1,66 @@
# syntax=docker/dockerfile:1.7
ARG GODOT_VERSION=4.6.3
FROM ubuntu:24.04 AS web-builder
ARG GODOT_VERSION
ARG SQLITE_VERSION=4.8
ARG TARGETARCH
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends ca-certificates curl libfontconfig1 unzip \
&& rm -rf /var/lib/apt/lists/*
RUN case "$TARGETARCH" in \
amd64) GODOT_ARCH="x86_64" ;; \
arm64) GODOT_ARCH="arm64" ;; \
*) echo "Unsupported build architecture: $TARGETARCH" >&2; exit 1 ;; \
esac \
&& curl -fsSL "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}-stable/Godot_v${GODOT_VERSION}-stable_linux.${GODOT_ARCH}.zip" -o /tmp/godot.zip \
&& unzip -q /tmp/godot.zip -d /usr/local/bin \
&& mv "/usr/local/bin/Godot_v${GODOT_VERSION}-stable_linux.${GODOT_ARCH}" /usr/local/bin/godot \
&& chmod +x /usr/local/bin/godot \
&& mkdir -p "/root/.local/share/godot/export_templates/${GODOT_VERSION}.stable" \
&& curl -fsSL "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}-stable/Godot_v${GODOT_VERSION}-stable_export_templates.tpz" -o /tmp/templates.tpz \
&& unzip -q /tmp/templates.tpz -d /tmp/godot-templates \
&& mv /tmp/godot-templates/templates/* "/root/.local/share/godot/export_templates/${GODOT_VERSION}.stable/" \
&& rm -rf /tmp/godot.zip /tmp/templates.tpz /tmp/godot-templates
WORKDIR /src
COPY . .
RUN mkdir -p addons/godot-sqlite/bin \
&& curl -fsSL "https://github.com/2shady4u/godot-sqlite/releases/download/v${SQLITE_VERSION}/addons.zip" -o /tmp/godot-sqlite.zip \
&& unzip -j /tmp/godot-sqlite.zip \
'addons/godot-sqlite/bin/libgdsqlite.web.template_release.wasm32.wasm' \
-d addons/godot-sqlite/bin \
&& rm /tmp/godot-sqlite.zip \
&& find assets -type f -name '*.import' -exec sed -i 's|^compress/mode=2$|compress/mode=0|' {} +
RUN mkdir -p dist \
&& godot --headless --path /src --import \
&& find .godot/imported -name 'title.png-*.ctex' ! -name '*.etc2.ctex' ! -name '*.s3tc.ctex' -type f -size +0c | grep -q . \
&& find .godot/imported -name 'kalulu_sprite_sheet.png-*.ctex' ! -name '*.etc2.ctex' ! -name '*.s3tc.ctex' -type f -size +0c | grep -q .
RUN godot --headless --path /src --export-release "Web Test" dist/index.html \
&& mv dist/index.pck dist/index-3.0.1-fr-joy.pck \
&& sed -i 's|"index.pck":|"index-3.0.1-fr-joy.pck":|; s|"focusCanvas":true,|"focusCanvas":true,"mainPack":"index-3.0.1-fr-joy.pck",|' dist/index.html \
&& test -s dist/index.html \
&& test -s dist/index.wasm \
&& test -s dist/index-3.0.1-fr-joy.pck
FROM alpine:3.22 AS french-content
ARG FRENCH_PACK_VERSION=v20260615-9e5d0d4
RUN apk add --no-cache ca-certificates curl \
&& mkdir -p /content \
&& curl -fsSL "https://github.com/Excello-Recherche-Education/Kalulu-Languages/releases/download/${FRENCH_PACK_VERSION}/fr_FR.zip" -o /content/fr_FR.zip \
&& test -s /content/fr_FR.zip
FROM nginx:1.29.1-alpine AS runtime
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=web-builder /src/dist/ /usr/share/nginx/html/
COPY --from=french-content /content/fr_FR.zip /usr/share/nginx/html/content/fr_FR.zip
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1:8080/healthz || exit 1
+18 -1
View File
@@ -2,6 +2,23 @@
# 📖 Kalulu — Apprends à lire intelligemment
## Version Web Docker autonome — Joy
Cette variante déploie Kalulu en français sur `https://kalulu.agendx.ch` avec un profil local **Joy**, 5 ans, niveau débutant (Grande Section). Elle nutilise pas lAPI officielle de Kalulu : le pack `fr_FR` est servi par la même image et la progression reste dans le stockage IndexedDB du navigateur.
Prérequis : Docker avec BuildKit, un réseau Docker externe `traefik_proxy` et un Traefik fournissant HTTPS.
```bash
docker network inspect traefik_proxy >/dev/null 2>&1 || docker network create traefik_proxy
docker compose build kalulu
docker compose up -d kalulu
./scripts/verify-live.sh
```
Le premier lancement charge environ 34 Mo dapplication puis télécharge 66 Mo de contenu pédagogique depuis la même origine. Le pack est ensuite extrait et conservé localement ; cette première installation peut prendre quelques minutes. Effacer les données du site dans le navigateur réinitialise le profil et la progression.
Limage construit lexport avec Godot 4.6.3, les modèles dexport officiels et Godot-SQLite 4.8 (la version compilée pour Godot 4.6.3). Nginx sert WebAssembly avec les en-têtes COOP/COEP requis pour les threads.
## 🌍 Langues disponibles
- 🇬🇧 [English](README.md)
@@ -126,4 +143,4 @@ Oui ! Des versions pour Windows, Mac et Linux sont disponibles dans la section [
### 🌐 Kalulu est-il disponible directement sur le web ?
Nous y travaillons, restez à l’écoute !
Oui, cette variante autonome est disponible sur [kalulu.agendx.ch](https://kalulu.agendx.ch).
+25
View File
@@ -0,0 +1,25 @@
services:
kalulu:
image: kalulu-web:3.0.1-fr-joy
build:
context: .
dockerfile: Dockerfile
container_name: kalulu-web
restart: unless-stopped
networks:
- traefik_proxy
labels:
- traefik.enable=true
- traefik.docker.network=traefik_proxy
- traefik.http.routers.kalulu.rule=Host(`kalulu.agendx.ch`)
- traefik.http.routers.kalulu.entrypoints=websecure
- traefik.http.routers.kalulu.tls=true
- traefik.http.routers.kalulu.tls.certresolver=myresolver
- traefik.http.routers.kalulu.middlewares=crowdsec-bouncer@docker
- traefik.http.services.kalulu.loadbalancer.server.port=8080
security_opt:
- no-new-privileges:true
networks:
traefik_proxy:
external: true
+6 -6
View File
@@ -38,7 +38,7 @@ architectures/armeabi-v7a=false
architectures/arm64-v8a=true
architectures/x86=false
architectures/x86_64=false
version/code=78
version/code=79
version/name=""
package/unique_name="org.godotengine.kalulu"
package/name="Kalulu"
@@ -264,7 +264,7 @@ architectures/armeabi-v7a=false
architectures/arm64-v8a=true
architectures/x86=false
architectures/x86_64=false
version/code=78
version/code=79
version/name=""
package/unique_name="org.godotengine.kalulu"
package/name="Kalulu"
@@ -490,7 +490,7 @@ architectures/armeabi-v7a=true
architectures/arm64-v8a=false
architectures/x86=false
architectures/x86_64=false
version/code=78
version/code=79
version/name=""
package/unique_name="org.godotengine.kalulu"
package/name="Kalulu"
@@ -1360,7 +1360,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../Export/web/Kalulu.html"
export_path="dist/index.html"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
@@ -1378,9 +1378,9 @@ script_export_mode=2
custom_template/debug=""
custom_template/release=""
variant/extensions_support=false
variant/extensions_support=true
variant/thread_support=true
vram_texture_compression/for_desktop=true
vram_texture_compression/for_desktop=false
vram_texture_compression/for_mobile=false
html/export_icon=true
html/custom_html_shell=""
+58
View File
@@ -0,0 +1,58 @@
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;
default_type application/octet-stream;
types {
text/html html;
text/css css;
application/javascript js;
application/wasm wasm;
application/json json;
image/png png;
image/svg+xml svg;
image/x-icon ico;
application/zip zip;
}
location = /healthz {
access_log off;
return 204;
}
location = /index.html {
add_header Cache-Control "no-cache" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
try_files $uri =404;
}
location /content/ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
try_files $uri =404;
}
location ~* \.(wasm|pck|js)$ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
try_files $uri =404;
}
location / {
try_files $uri $uri/ =404;
}
}
+2
View File
@@ -23,6 +23,7 @@ boot_splash/bg_color=Color(0.141176, 0.141176, 0.141176, 1)
config/icon="res://assets/kalulu_icon.png"
custom/unlock_everything=false
custom/unlock_everything.debug=false
custom/local_web_mode=true
[audio]
@@ -36,6 +37,7 @@ Globals="*res://sources/language_tool/globals.gd"
LessonLogger="*res://sources/utils/autoloads/lesson_logger.gd"
MusicManager="*res://sources/utils/autoloads/music_manager.tscn"
OpeningCurtain="*res://sources/utils/autoloads/opening_curtain.tscn"
SceneLoader="*res://sources/utils/autoloads/scene_loader.gd"
ServerManager="*res://sources/utils/autoloads/server_manager.tscn"
UnicodeNormalizer="*res://sources/utils/autoloads/unicode_normalizer.gd"
UserDataManager="*res://sources/utils/autoloads/user_data_manager.gd"
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
set -eu
BASE_URL="${1:-https://kalulu.agendx.ch}"
HEADERS="$(mktemp)"
trap 'rm -f "$HEADERS"' EXIT
curl -fsS -D "$HEADERS" -o /dev/null "$BASE_URL/"
grep -qi '^cross-origin-opener-policy: same-origin' "$HEADERS"
grep -qi '^cross-origin-embedder-policy: require-corp' "$HEADERS"
curl -fsS -o /dev/null "$BASE_URL/index.wasm"
curl -fsS -r 0-1023 -o /dev/null "$BASE_URL/content/fr_FR.zip"
printf 'Kalulu Web OK: %s\n' "$BASE_URL"
+32 -35
View File
@@ -20,21 +20,25 @@ const LESSON_BUTTON_OUTLINE_RADIUS: float = 192.0
# button center, mirroring the movie icon just above it. The 384x384 button has
# its center at y=192; the label (vertical-centered) then centers at y=(this+384)/2.
const LESSON_BUTTON_LABEL_TOP_OFFSET: float = 104.0
const GARDEN_SCENES: Array[PackedScene] = [
preload("res://resources/gardens/garden_01.tscn"),
preload("res://resources/gardens/garden_02.tscn"),
preload("res://resources/gardens/garden_03.tscn"),
preload("res://resources/gardens/garden_04.tscn"),
preload("res://resources/gardens/garden_05.tscn"),
preload("res://resources/gardens/garden_06.tscn"),
preload("res://resources/gardens/garden_07.tscn"),
preload("res://resources/gardens/garden_08.tscn"),
preload("res://resources/gardens/garden_09.tscn"),
preload("res://resources/gardens/garden_10.tscn"),
preload("res://resources/gardens/garden_11.tscn"),
preload("res://resources/gardens/garden_12.tscn"),
# Loaded on demand instead of preloaded: this script never unloads (it has
# static variables), so preloaded constants would pin every garden's assets in
# memory for the whole app lifetime — including while minigames run, which
# OOM-crashes low-memory devices.
const GARDEN_SCENE_PATHS: Array[String] = [
"res://resources/gardens/garden_01.tscn",
"res://resources/gardens/garden_02.tscn",
"res://resources/gardens/garden_03.tscn",
"res://resources/gardens/garden_04.tscn",
"res://resources/gardens/garden_05.tscn",
"res://resources/gardens/garden_06.tscn",
"res://resources/gardens/garden_07.tscn",
"res://resources/gardens/garden_08.tscn",
"res://resources/gardens/garden_09.tscn",
"res://resources/gardens/garden_10.tscn",
"res://resources/gardens/garden_11.tscn",
"res://resources/gardens/garden_12.tscn",
]
const LOOK_AND_LEARN_SCENE: PackedScene = preload("res://sources/look_and_learn/look_and_learn.tscn")
const LOOK_AND_LEARN_SCENE_PATH: String = "res://sources/look_and_learn/look_and_learn.tscn"
const BOSS_BUTTON_SCENE: PackedScene = preload("res://sources/gardens/boss_button.tscn")
const BOSS_MINIGAME_SCENE_PATH: String = "res://sources/minigames/boss/boss_minigame.tscn"
const GARDEN_SIZE: int = 2400
@@ -71,7 +75,6 @@ static var cached_layout_lessons: int = 0
@export var minigames_body_icons: Array[Texture] = []
@export var minigames_face_icons: Array[Texture] = []
var _minigame_scene_cache: Dictionary = {}
var lessons: Dictionary = {}
var _gardens_layout: GardensLayout
var points: Array[Array] = []
@@ -988,20 +991,13 @@ func _lesson_button_label_color(status: StudentProgression.Status) -> Color:
return lesson_button.unlocked_label_color
func _get_minigame_scene(scene_index: int) -> PackedScene:
func _get_minigame_scene_path(scene_index: int) -> String:
if scene_index < 0 or scene_index >= minigame_scene_paths.size():
return null
if _minigame_scene_cache.has(scene_index):
return _minigame_scene_cache[scene_index] as PackedScene
return ""
var scene_path: String = minigame_scene_paths[scene_index]
if scene_path.is_empty():
return null
var scene_resource: Resource = load(scene_path)
if scene_resource is PackedScene:
var packed_scene: PackedScene = scene_resource as PackedScene
_minigame_scene_cache[scene_index] = packed_scene
return packed_scene
return null
if scene_path.is_empty() or not ResourceLoader.exists(scene_path):
return ""
return scene_path
func _count_completed_minigames(lesson_number: int) -> int:
@@ -1073,7 +1069,8 @@ func add_gardens() -> void:
for layout_index: int in range(gardens_layout.gardens.size()):
Log.trace("Gardens: Preparing garden %s with layout index %s" % [str(garden_index), str(layout_index)])
var garden_layout: GardenLayout = gardens_layout.gardens[layout_index]
var garden: Garden = GARDEN_SCENES[layout_index].instantiate()
var garden_scene: PackedScene = load(GARDEN_SCENE_PATHS[layout_index]) as PackedScene
var garden: Garden = garden_scene.instantiate()
garden_parent.add_child(garden)
garden.garden_index = garden_index
garden_index += 1
@@ -1422,7 +1419,7 @@ func _on_lesson_button_pressed() -> void:
current_garden_index = current_garden.garden_index,
look_and_learn_completed = false
}
get_tree().change_scene_to_packed(LOOK_AND_LEARN_SCENE)
SceneLoader.change_scene(LOOK_AND_LEARN_SCENE_PATH)
func _on_boss_button_pressed(lesson_number: int, garden_index: int) -> void:
@@ -1438,7 +1435,7 @@ func _on_boss_button_pressed(lesson_number: int, garden_index: int) -> void:
skip_minigame_layout = true,
boss_gate_lesson = lesson_number
}
get_tree().change_scene_to_file(BOSS_MINIGAME_SCENE_PATH)
SceneLoader.change_scene(BOSS_MINIGAME_SCENE_PATH)
func _on_final_boss_button_pressed(lesson_number: int, garden_index: int) -> void:
@@ -1455,14 +1452,14 @@ func _on_final_boss_button_pressed(lesson_number: int, garden_index: int) -> voi
boss_gate_lesson = lesson_number,
is_final_boss = true
}
get_tree().change_scene_to_file(BOSS_MINIGAME_SCENE_PATH)
SceneLoader.change_scene(BOSS_MINIGAME_SCENE_PATH)
func _on_minigame_button_pressed(scene_index: int, minigame_number: int) -> void:
if is_locked:
return
var minigame_scene: PackedScene = _get_minigame_scene(scene_index)
if not minigame_scene:
var scene_path: String = _get_minigame_scene_path(scene_index)
if scene_path.is_empty():
Log.error("Gardens: Missing minigame scene for index %d" % scene_index)
return
# Block a second wedge click during the curtain-close await below.
@@ -1476,7 +1473,7 @@ func _on_minigame_button_pressed(scene_index: int, minigame_number: int) -> void
minigame_number = minigame_number,
minigame_completed = false
}
get_tree().change_scene_to_packed(minigame_scene)
SceneLoader.change_scene(scene_path)
func _on_scroll_container_gui_input(event: InputEvent) -> void:
@@ -1515,7 +1512,7 @@ func _scroll_by_garden(p_direction: int) -> void:
func _confirm_back_button_pressed() -> void:
UserDataManager.logout_student()
await (OpeningCurtain as OpeningCurtainClass).close()
get_tree().change_scene_to_file("res://sources/menus/login/login.tscn")
SceneLoader.change_scene("res://sources/menus/login/login.tscn")
func _on_back_button_button_down() -> void:
+1 -1
View File
@@ -160,7 +160,7 @@ func _back_to_gardens() -> void:
Log.info("LookAndLearn: Returning to gardens for lesson %d" % lesson_nb)
await (OpeningCurtain as OpeningCurtainClass).close()
Gardens.transition_data = gardens_data
get_tree().change_scene_to_file("res://sources/gardens/gardens.tscn")
SceneLoader.change_scene("res://sources/gardens/gardens.tscn")
func _on_back_button_pressed() -> void:
@@ -15,6 +15,12 @@ var teacher_settings: TeacherSettings
func _ready() -> void:
await get_tree().process_frame
if (ServerManager as ServerManagerClass).is_local_web_mode():
UserDataManager.ensure_joy_local_profile()
UserDataManager.set_language("fr_FR", true)
UserDataManager.save_all()
go_to_package_download()
return
device_language = UserDataManager.get_device_settings().language
Log.trace("LanguageCheck: Starting with device language %s" % device_language)
# Check the teacher settings, if we are logged in (this scene should not be accessible otherwise)
@@ -15,6 +15,9 @@ const MAIN_MENU_SCENE_PATH: String = "res://sources/menus/main/main_menu.tscn"
const DEVICE_SELECTION_SCENE_PATH: String = "res://sources/menus/device_selection/device_selection.tscn"
const LOGIN_SCENE_PATH: String = "res://sources/menus/login/login.tscn"
const USER_LANGUAGE_RESOURCES_PATH: String = "user://language_resources"
const LOCAL_WEB_PACK_PATH: String = "/content/fr_FR.zip"
const LOCAL_WEB_PACK_VERSION: Dictionary = {"year": 2026, "month": 6, "day": 15}
const GARDENS_SCENE_PATH: String = "res://sources/gardens/gardens.tscn"
# Translation key shown in the error popup for each DownloadError value
const ERROR_MESSAGES: Array[String] = [
"DISCONNECTED_ERROR",
@@ -63,6 +66,12 @@ func _ready() -> void:
Log.trace("PackageDownloader: Using language from teacherSettings: %s" % language)
current_language_path = USER_LANGUAGE_RESOURCES_PATH.path_join(language)
current_language_version = UserDataManager.get_device_settings().language_versions.get(language, {})
if (ServerManager as ServerManagerClass).is_local_web_mode():
server_language_version = LOCAL_WEB_PACK_VERSION
var origin: String = str(JavaScriptBridge.eval("window.location.origin"))
_start_pack_if_needed(origin + LOCAL_WEB_PACK_PATH)
return
Log.trace("PackageDownloader: Checking internet access")
if not await ServerManager.check_internet_access():
@@ -97,6 +106,10 @@ func _ready() -> void:
_show_error(DownloadError.DOWNLOAD_FAILED)
return
_start_pack_if_needed(res.body.url as String)
func _start_pack_if_needed(pack_url: String) -> void:
# If the language pack is not already downloaded or an update is needed
if not DirAccess.dir_exists_absolute(current_language_path) or current_language_version != server_language_version:
Log.trace("PackageDownloader: A new version of the language pack has been detected.\n Current version = " + str(current_language_version) + "\n Server version = " + str(server_language_version))
@@ -116,8 +129,8 @@ func _ready() -> void:
# still run offline if the download fails; it is only removed during
# extraction, once the new pack has been fully downloaded.
http_request.set_download_file(USER_LANGUAGE_RESOURCES_PATH.path_join(language + ".zip"))
Log.trace("PackageDownloader: Downloading pack from %s" % res.body.url)
var request_error: Error = http_request.request(res.body.url as String)
Log.trace("PackageDownloader: Downloading pack from %s" % pack_url)
var request_error: Error = http_request.request(pack_url)
if request_error != OK:
Log.error("PackageDownloader: Cannot start language pack download: %s" % error_string(request_error))
_show_error(DownloadError.DOWNLOAD_FAILED)
@@ -255,6 +268,14 @@ func _go_to_next_scene() -> void:
if not server_language_version.is_empty():
UserDataManager.set_language_version(language, server_language_version)
if (ServerManager as ServerManagerClass).is_local_web_mode():
if not UserDataManager.login_student("123"):
Log.error("PackageDownloader: Could not open Joy local profile")
_show_error(DownloadError.DISCONNECTED)
return
SceneLoader.change_scene(GARDENS_SCENE_PATH)
return
# Check if we have a valid device id
if not UserDataManager.get_device_settings().device_id:
+2 -3
View File
@@ -2,7 +2,7 @@ extends Control
const TEACHER_PASSWORD: String = "42"
const BACK_SCENE_PATH: String = "res://sources/menus/main/main_menu.tscn"
const NEXT_SCENE: PackedScene = preload("res://sources/gardens/gardens.tscn")
const NEXT_SCENE_PATH: String = "res://sources/gardens/gardens.tscn"
const TEACHER_SCENE_PATH: String = "res://sources/menus/settings/teacher_settings.tscn"
const DEVELOPER_SCENE_PATH: String = "res://sources/menus/settings/developer_settings.tscn"
const PACKAGE_LOADER_SCENE_PATH: String = "res://sources/menus/language_selection/package_downloader.tscn"
@@ -73,8 +73,7 @@ func _on_code_keyboard_password_entered(password: String) -> void:
await kalulu.play_kalulu_speech(right_password_speech)
await OpeningCurtain.close()
Log.trace("LoginScreen: Start loading next scene")
get_tree().change_scene_to_packed(NEXT_SCENE)
Log.trace("LoginScreen: End loading next scene")
SceneLoader.change_scene(NEXT_SCENE_PATH)
else:
Log.warn("LoginScreen: Unknown student code entered (length=%d)" % password.length())
kalulu_button.hide()
+4
View File
@@ -22,6 +22,7 @@ var dev_last_click_time: float = 0.0
func _ready() -> void:
UserDataManager.ensure_joy_local_profile()
Log.info("MainMenu: Loaded successfuly")
version_label.text = Utils.get_application_version_with_code()
teacher_label.text = UserDataManager.get_device_settings().teacher
@@ -33,6 +34,9 @@ func _ready() -> void:
func _on_main_button_pressed() -> void:
Log.info("MainMenu: Main button pressed")
play_button.set_disabled(true)
if (ServerManager as ServerManagerClass).is_local_web_mode():
_on_login_in()
return
if UserDataManager.get_device_settings().teacher:
Log.info("MainMenu: Device settings has teacher info, so start auto login-in")
_on_login_in()
+1 -1
View File
@@ -435,7 +435,7 @@ func _go_back_to_the_garden() -> void:
_save_logs()
Gardens.transition_data = gardens_data
get_tree().change_scene_to_file("res://sources/gardens/gardens.tscn")
SceneLoader.change_scene("res://sources/gardens/gardens.tscn")
func _play_stimulus() -> void:
+1 -2
View File
@@ -5,8 +5,7 @@ signal lilypad_in_center(lilypad: Lilypad)
const LILYPAD_SCENE_PATH: String = "res://sources/minigames/frog/lilypad.tscn"
static var _lilypad_scene: PackedScene
var _lilypad_scene: PackedScene
var top_to_bottom: bool = false
var is_stopped: bool = false
var is_cleared: bool = false
+8 -2
View File
@@ -38,14 +38,20 @@ var words_path: String = get_language_folder() + "/words/"
var additional_word_list: Dictionary = {}
var is_open: bool = false
@onready var db: SQLite = SQLite.new()
@onready var db: Object = (
ClassDB.instantiate(&"SQLite") if ClassDB.class_exists(&"SQLite") else null
)
func _exit_tree() -> void:
db.close_db()
if db:
db.close_db()
func connect_to_db() -> void:
if not db:
Log.error("Database: SQLite extension is not available")
return
if is_open:
Log.trace("Database: Opening database while a connection is already opened. Closing the previous one.")
db.close_db()
+70
View File
@@ -0,0 +1,70 @@
extends Node
## Scene changer that keeps peak memory low on low-end devices.
##
## [method SceneTree.change_scene_to_file] loads the new scene while the old
## one is still fully resident, so transitions between heavy scenes (gardens,
## minigames) momentarily need the memory of both — enough to OOM-crash old
## tablets. This helper frees the current scene first, then streams the new
## scene from disk on a background thread so the main thread keeps rendering
## frames instead of freezing.
##
## Callers are expected to have covered the screen (OpeningCurtain closed)
## before calling [method change_scene]; the curtain stays visible while the
## tree has no current scene.
## Scene shown if the requested scene fails to load, so the player is never
## stuck on a black screen.
const FALLBACK_SCENE_PATH: String = "res://sources/menus/main/main_menu.tscn"
var _is_changing: bool = false
func change_scene(scene_path: String) -> void:
if _is_changing:
Log.warn("SceneLoader: Change to %s requested while another change is in progress; ignoring" % scene_path)
return
if not ResourceLoader.exists(scene_path):
Log.error("SceneLoader: Scene does not exist: %s" % scene_path)
return
_is_changing = true
# Defer so the calling scene is never freed while one of its methods is
# still on the stack.
_change_scene_deferred.call_deferred(scene_path)
func _change_scene_deferred(scene_path: String) -> void:
var tree: SceneTree = get_tree()
# Free the old scene before loading the new one: this roughly halves the
# memory peak of the transition.
if tree.current_scene:
tree.unload_current_scene()
var error: Error = ResourceLoader.load_threaded_request(scene_path)
if error != OK:
Log.error("SceneLoader: Could not start loading %s (error %s)" % [scene_path, error_string(error)])
_finish_with_fallback(tree, scene_path)
return
var status: ResourceLoader.ThreadLoadStatus = ResourceLoader.load_threaded_get_status(scene_path)
while status == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
await tree.process_frame
status = ResourceLoader.load_threaded_get_status(scene_path)
if status != ResourceLoader.THREAD_LOAD_LOADED:
Log.error("SceneLoader: Failed to load %s (status %d)" % [scene_path, status])
_finish_with_fallback(tree, scene_path)
return
var packed_scene: PackedScene = ResourceLoader.load_threaded_get(scene_path) as PackedScene
if not packed_scene:
Log.error("SceneLoader: %s is not a PackedScene" % scene_path)
_finish_with_fallback(tree, scene_path)
return
error = tree.change_scene_to_packed(packed_scene)
if error != OK:
Log.error("SceneLoader: Could not change to %s (error %s)" % [scene_path, error_string(error)])
_finish_with_fallback(tree, scene_path)
return
_is_changing = false
func _finish_with_fallback(tree: SceneTree, failed_scene_path: String) -> void:
_is_changing = false
if failed_scene_path != FALLBACK_SCENE_PATH:
tree.change_scene_to_file(FALLBACK_SCENE_PATH)
@@ -0,0 +1 @@
uid://bm3xnmft2e4ul
@@ -25,6 +25,10 @@ var environment_setting: int = 1
@onready var loading_rect: TextureRect = $TextureRect
func is_local_web_mode() -> bool:
return OS.has_feature("web") and bool(ProjectSettings.get_setting("application/custom/local_web_mode", false))
func _ready() -> void:
var config: ConfigFile = ConfigFile.new()
var load_error: Error = config.load(CONFIG_PATH)
@@ -84,6 +88,8 @@ func _save_environment_config() -> void:
func first_login_student() -> void:
if is_local_web_mode():
return
await _post_json_request("submit_student_session", {"student_id": UserDataManager.student})
@@ -185,6 +191,8 @@ func reset_language(language: String) -> Dictionary:
#region Sender functions
func check_internet_access() -> bool:
if is_local_web_mode():
return false
Log.trace("ServerManager: Sending simple request to " + INTERNET_CHECK_URL + " to check if internet is available")
var res: Error = internet_check.request(INTERNET_CHECK_URL)
if res == OK:
@@ -107,6 +107,8 @@ func _process(_delta: float) -> void:
#region synchronization
func start_synchronization_timer() -> void:
if (ServerManager as ServerManagerClass).is_local_web_mode():
return
if not synchronization_timer_running:
synchronization_timer = 0
synchronization_timer_running = true
@@ -117,6 +119,52 @@ func stop_synchronization_timer() -> void:
synchronization_timer_running = false
Log.trace("UserDataManager: Synchronization timer stopped")
func ensure_joy_local_profile() -> void:
if not (ServerManager as ServerManagerClass).is_local_web_mode():
return
var settings: DeviceSettings = get_device_settings()
settings.language = "fr_FR"
settings.teacher = "profil-local"
settings.device_id = 1
_save_device_settings()
var teacher_path: String = get_teacher_settings_path()
if FileAccess.file_exists(teacher_path):
_load_teacher_settings()
if not teacher_settings:
teacher_settings = TeacherSettings.new()
teacher_settings.account_type = TeacherSettings.AccountType.PARENT
teacher_settings.education_method = TeacherSettings.EducationMethod.APP_ONLY
teacher_settings.email = "profil-local"
teacher_settings.token = "local-only"
teacher_settings.language = "fr_FR"
teacher_settings.server_language_validated = true
var joy: StudentData = StudentData.new()
joy.code = 123
joy.name = "Joy"
joy.age = 5
joy.level = StudentData.Level.BEGINNER
var joy_students: Array[StudentData] = [joy]
teacher_settings.students[1] = joy_students
DirAccess.make_dir_recursive_absolute(get_teacher_folder())
save_teacher_settings()
else:
teacher_settings.language = "fr_FR"
teacher_settings.server_language_validated = true
if not teacher_settings.students.has(1) or (teacher_settings.students[1] as Array).is_empty():
var joy: StudentData = StudentData.new()
joy.code = 123
joy.name = "Joy"
joy.age = 5
joy.level = StudentData.Level.BEGINNER
var joy_students: Array[StudentData] = [joy]
teacher_settings.students[1] = joy_students
save_teacher_settings()
TranslationServer.set_locale("fr_FR")
#endregion
#region Registering and logging in
+1
View File
@@ -0,0 +1 @@
uid://34wj4ytlkuvn