creatures keep their first corner, and characters never share files

- a creature walking a path no longer cuts its first corner: the path's first waypoint was dropped at decode (2342)
- two characters whose names differ only in accented or non-Latin letters no longer share macros, bindings and SavedVariables; a file that failed to load is not overwritten at logout (2342)
- custom chat channels survive a logout, a collapsed quest header keeps its quests watched, and a trackpad swipe scrolls as far as it adds up to (2342)
- a hearth off a zeppelin no longer lands back on its deck, and an opened lockbox with loot left in it unlocks (2342)
- every snapshot runs on Vulkan, in a container, before it ships (2344)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-23 12:19:22 +07:00
co-authored by Claude Opus 5.5
parent 9c49f10882
commit 293b8e57fb
60 changed files with 1876 additions and 425 deletions
+29 -9
View File
@@ -717,8 +717,18 @@ fn load_saved_variables(lua: &Lua, i: usize) {
}
let Some(dir) = dir else { continue };
let path = dir.join(format!("{name}.lua"));
let Ok(bytes) = std::fs::read(&path) else {
continue; // absent is the first-run case
let bytes = match std::fs::read(&path) {
Ok(bytes) => bytes,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue, // the first-run case
Err(e) => {
// Present but unreadable is not "absent": hold it, like a file that will not run.
log_error(lua, &format!("{}: {e}", path.display()));
let mut model = lua.app_data_mut::<Model>().expect("model");
if !model.held_saved_files.contains(&path) {
model.held_saved_files.push(path);
}
continue;
}
};
if let Err(e) = run_chunk(
lua,
@@ -726,8 +736,14 @@ fn load_saved_variables(lua: &Lua, i: usize) {
&crate::script::addon_chunk_name(&name.to_string(), &format!("{name}.lua")),
) {
// The reference fails this silently. We do not: a settings file that stopped parsing
// is exactly the thing a player needs told, and the file is left on disk untouched.
// is exactly the thing a player needs told, and the file is left on disk untouched —
// held, so the shutdown write does not replace it with the defaults this session is
// running on (`UiScript::hold_saved_file`).
log_error(lua, &format!("{}: {e}", path.display()));
let mut model = lua.app_data_mut::<Model>().expect("model");
if !model.held_saved_files.contains(&path) {
model.held_saved_files.push(path);
}
}
}
}
@@ -839,13 +855,17 @@ fn run_chunk(lua: &Lua, bytes: &[u8], name: &str) -> mlua::Result<()> {
.exec()
}
/// Record a load error where the host drains it — the same channel a handler error uses, so a
/// demand-load failure surfaces the way every other script error does rather than vanishing.
/// Record a demand-load failure the way the startup walk records its own
/// ([`super::UiScript::report_script_error`]): retained as a Load row, and dispatched to the
/// player's error handler — plus the host drain the instruments read. It went to that drain alone
/// until now, so a `LoadAddOn` whose file raised surfaced as a terminal line and nothing a player
/// could see.
fn log_error(lua: &Lua, msg: &str) {
lua.app_data_mut::<Model>()
.expect("model")
.errors
.push(format!("LoadAddOn: {msg}"));
let msg = format!("LoadAddOn: {msg}");
super::diagnostics::record_load_failure(lua, &msg);
let mut model = lua.app_data_mut::<Model>().expect("model");
model.pending_error_dispatch.push(msg.clone());
model.errors.push(msg);
}
/// The host's push: the discovered registry and the root its files live under.