- orchestration.rs:同步子系统编排核心(能力判定/全局与实例开关/重协调) - command_history.rs + hotbars.rs:命令历史与创造模式快捷栏同步 - synced_packs_axolotl.rs:资源包/数据包共享投影(单文件实现) - resource_pack_options.rs:resourcesPacks 列表读写与合并(synced_packs 依赖) - 解决循环依赖:synced_options_path 归入 files.rs;SyncedOptionJoinResolution 归入 pending.rs - instance_rows 补 set_instance_sync_preference;api/instance/mod.rs 补 synced_packs 内联模块与三块 pub use - 6 处 sqlx::query! 宏改写为非宏 已知:未编译验证;local_settings_editor 尚未搬入。
This commit is contained in:
1 parent
ed61a5942c
commit
6e9485c2f5
9 files changed
+3739
-1
No files matched your search
@@ -17,6 +17,13 @@ mod screenshot_groups;
|
||||
mod screenshots;
|
||||
mod upgrade;
|
||||
pub(crate) mod synced_options;
|
||||
pub(crate) mod synced_packs {
|
||||
pub(crate) use super::synced_packs_axolotl::{
|
||||
capture_resource_pack_selection_change, detach,
|
||||
prepare_instance_update, reconcile, seed_from_instance,
|
||||
};
|
||||
}
|
||||
mod synced_packs_axolotl;
|
||||
pub(crate) mod synced_servers;
|
||||
|
||||
pub use self::backup::{
|
||||
@@ -116,6 +123,36 @@ pub use self::screenshots::{
|
||||
list_all_screenshots, list_screenshots, list_synced_screenshots,
|
||||
move_screenshots, save_edited_screenshot,
|
||||
};
|
||||
pub(crate) use self::synced_options::game_options::{
|
||||
apply_launcher_overrides as apply_game_options_launcher_overrides,
|
||||
sync_before_launch as sync_game_options_before_launch,
|
||||
};
|
||||
pub use self::synced_options::{
|
||||
GlobalSyncedOptions, SyncedOptionCapability, SyncedOptionJoinAction,
|
||||
SyncedOptionJoinPreview, SyncedOptionJoinResolution, SyncedOptionsOverview,
|
||||
get_capabilities as get_synced_option_capabilities,
|
||||
get_command_history as get_synced_command_history,
|
||||
get_global_options as get_global_synced_options,
|
||||
get_initialized_options as get_initialized_synced_options,
|
||||
get_instance_option_join_preview as get_synced_option_join_preview,
|
||||
get_overview as get_synced_options_overview,
|
||||
set_command_history as set_synced_command_history,
|
||||
set_global_option as set_global_synced_option,
|
||||
set_instance_option as set_instance_synced_option,
|
||||
};
|
||||
pub(crate) use self::synced_options::{
|
||||
reconcile_changed_file as reconcile_synced_option_file,
|
||||
remove_generated_instance_files,
|
||||
};
|
||||
pub use self::synced_packs_axolotl::{
|
||||
PackSyncPreview, PackSyncTarget, desync_pack, get_pack_sync_preview,
|
||||
list_synced_packs, remove_synced_pack, set_synced_pack_enabled, sync_pack,
|
||||
upload_synced_pack,
|
||||
};
|
||||
pub use self::synced_servers::{
|
||||
DesyncServerMode, ServerSource, SyncedServer, desync_server,
|
||||
list_synced_servers, remove_synced_server, update_synced_server,
|
||||
};
|
||||
pub use self::upgrade::{
|
||||
dismiss_instance_post_upgrade_notice, execute_instance_upgrade,
|
||||
get_instance_post_upgrade_notice, get_instance_upgrade_plan,
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
use super::COMMAND_HISTORY_FILE;
|
||||
use super::files::{
|
||||
CheckpointStatus, begin_checkpoint, checkpoint, ensure_link,
|
||||
finish_checkpoint, instance_game_dir, sha1_bytes, sha1_file,
|
||||
};
|
||||
use super::orchestration::{
|
||||
create_synced_directories, option_effective, synced_options_path,
|
||||
};
|
||||
use crate::State;
|
||||
use crate::state::{InstanceMetadata, SyncedOption};
|
||||
use crate::util::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const COMMAND_HISTORY_LIMIT: usize = 50;
|
||||
|
||||
pub async fn get_command_history() -> crate::Result<String> {
|
||||
let state = State::get().await?;
|
||||
let _guard = state.lock_synced_options().await;
|
||||
let path = command_history_path(&state);
|
||||
if !path.exists() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&io::read(path).await?).into_owned())
|
||||
}
|
||||
|
||||
pub async fn set_command_history(contents: &str) -> crate::Result<String> {
|
||||
let state = State::get().await?;
|
||||
let _guard = state.lock_synced_options().await;
|
||||
create_synced_directories(&state).await?;
|
||||
let normalized = normalize_command_history(contents);
|
||||
io::write(command_history_path(&state), normalized.as_bytes()).await?;
|
||||
refresh_command_history_links(&state).await?;
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
pub(super) async fn ensure_command_history(
|
||||
metadata: &InstanceMetadata,
|
||||
state: &State,
|
||||
) -> crate::Result<()> {
|
||||
create_synced_directories(state).await?;
|
||||
let canonical = command_history_path(state);
|
||||
if !canonical.exists() {
|
||||
let local =
|
||||
instance_game_dir(metadata, state)?.join(COMMAND_HISTORY_FILE);
|
||||
let contents = if local.exists() {
|
||||
String::from_utf8_lossy(&io::read(&local).await?).into_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
io::write(&canonical, normalize_command_history(&contents)).await?;
|
||||
}
|
||||
let target = instance_game_dir(metadata, state)?.join(COMMAND_HISTORY_FILE);
|
||||
let canonical_bytes = io::read(&canonical).await?;
|
||||
let expected = sha1_bytes(&canonical_bytes);
|
||||
begin_checkpoint(
|
||||
&metadata.instance.id,
|
||||
SyncedOption::CommandHistory,
|
||||
"default",
|
||||
&expected,
|
||||
None,
|
||||
0,
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
let mode = ensure_link(&canonical, &target).await?;
|
||||
finish_checkpoint(
|
||||
&metadata.instance.id,
|
||||
SyncedOption::CommandHistory,
|
||||
"default",
|
||||
mode,
|
||||
state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn reconcile_command_history(
|
||||
metadata: &InstanceMetadata,
|
||||
state: &State,
|
||||
) -> crate::Result<()> {
|
||||
if !option_effective(metadata, SyncedOption::CommandHistory, state).await? {
|
||||
return Ok(());
|
||||
}
|
||||
let local = instance_game_dir(metadata, state)?.join(COMMAND_HISTORY_FILE);
|
||||
if !local.exists() {
|
||||
return ensure_command_history(metadata, state).await;
|
||||
}
|
||||
let symlink = tokio::fs::symlink_metadata(&local)
|
||||
.await
|
||||
.map(|metadata| metadata.file_type().is_symlink())
|
||||
.unwrap_or(false);
|
||||
let current_checkpoint = checkpoint(
|
||||
&metadata.instance.id,
|
||||
SyncedOption::CommandHistory,
|
||||
"default",
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
if current_checkpoint
|
||||
.as_ref()
|
||||
.is_some_and(|value| value.status == CheckpointStatus::Pending)
|
||||
{
|
||||
return ensure_command_history(metadata, state).await;
|
||||
}
|
||||
let actual = sha1_file(&local).await?;
|
||||
let expected = current_checkpoint.map(|value| value.expected_sha1);
|
||||
if !symlink && expected.as_deref() != Some(actual.as_str()) {
|
||||
let contents =
|
||||
String::from_utf8_lossy(&io::read(&local).await?).into_owned();
|
||||
io::write(
|
||||
command_history_path(state),
|
||||
normalize_command_history(&contents),
|
||||
)
|
||||
.await?;
|
||||
refresh_command_history_links(state).await?;
|
||||
} else {
|
||||
ensure_command_history(metadata, state).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn refresh_command_history_links(state: &State) -> crate::Result<()> {
|
||||
let instances = crate::state::list_instances(&state.pool).await?;
|
||||
for metadata in instances {
|
||||
if option_effective(&metadata, SyncedOption::CommandHistory, state)
|
||||
.await?
|
||||
{
|
||||
ensure_command_history(&metadata, state).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn normalize_command_history(contents: &str) -> String {
|
||||
let lines = contents.lines().collect::<Vec<_>>();
|
||||
let start = lines.len().saturating_sub(COMMAND_HISTORY_LIMIT);
|
||||
let mut normalized = lines[start..].join("\n");
|
||||
if !normalized.is_empty() {
|
||||
normalized.push('\n');
|
||||
}
|
||||
normalized
|
||||
}
|
||||
|
||||
pub(super) async fn merge_command_history_from_instance(
|
||||
metadata: &InstanceMetadata,
|
||||
state: &State,
|
||||
) -> crate::Result<()> {
|
||||
let canonical_path = command_history_path(state);
|
||||
let canonical = if canonical_path.exists() {
|
||||
String::from_utf8_lossy(&io::read(&canonical_path).await?).into_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let local_path =
|
||||
instance_game_dir(metadata, state)?.join(COMMAND_HISTORY_FILE);
|
||||
let local = if local_path.exists() {
|
||||
String::from_utf8_lossy(&io::read(&local_path).await?).into_owned()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let canonical_lines = canonical.lines().collect::<Vec<_>>();
|
||||
let available = COMMAND_HISTORY_LIMIT.saturating_sub(canonical_lines.len());
|
||||
let mut seen = canonical_lines
|
||||
.iter()
|
||||
.copied()
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
let mut imported = Vec::new();
|
||||
for line in local.lines().rev() {
|
||||
if imported.len() == available {
|
||||
break;
|
||||
}
|
||||
if seen.insert(line) {
|
||||
imported.push(line);
|
||||
}
|
||||
}
|
||||
imported.reverse();
|
||||
imported.extend(canonical_lines);
|
||||
let merged = normalize_command_history(&imported.join("\n"));
|
||||
if merged != normalize_command_history(&canonical) {
|
||||
io::write(&canonical_path, merged).await?;
|
||||
refresh_command_history_links(state).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn command_history_path(state: &State) -> PathBuf {
|
||||
synced_options_path(state).join(COMMAND_HISTORY_FILE)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ pub(crate) mod locales;
|
||||
mod options_file;
|
||||
mod pack_updates;
|
||||
mod read_instance_changes;
|
||||
mod resource_pack_options;
|
||||
mod settings_editor;
|
||||
mod source_selection;
|
||||
mod write_shared_settings;
|
||||
@@ -54,6 +55,10 @@ pub(crate) use write_shared_settings::sync_all_participating_instances;
|
||||
pub(in crate::api::instance) use pack_updates::{
|
||||
detach_instance, prepare_instance_update_with_state,
|
||||
};
|
||||
pub(in crate::api::instance) use resource_pack_options::{
|
||||
ResourcePackOptionsUpdate, merge_resource_pack_entries,
|
||||
merge_resource_pack_order, read_resource_pack_entries,
|
||||
};
|
||||
pub(in crate::api::instance) use source_selection::initialize_from_source_instance;
|
||||
pub(in crate::api::instance) use write_shared_settings::{
|
||||
apply_instance_with_state, capture_instance_file_change,
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
//! Reads and merges Minecraft's active resource-pack list.
|
||||
|
||||
use super::super::{instance_is_running, sync_files_are_protected};
|
||||
use super::options_file::{input_error, options_path, read_document};
|
||||
use crate::state::{InstanceMetadata, State};
|
||||
use crate::util::io;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(in crate::api::instance) struct ResourcePackOptions {
|
||||
pub entries: Vec<String>,
|
||||
pub incompatible: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(in crate::api::instance) enum ResourcePackOptionsUpdate {
|
||||
Deferred,
|
||||
Applied(ResourcePackOptions),
|
||||
}
|
||||
|
||||
fn parse_entries(raw: &str) -> crate::Result<Vec<String>> {
|
||||
serde_json::from_str(raw).map_err(|_| {
|
||||
input_error("options.txt contains an invalid resourcePacks value")
|
||||
})
|
||||
}
|
||||
|
||||
pub(in crate::api::instance) fn merge_resource_pack_order(
|
||||
entries: &[String],
|
||||
managed: &BTreeSet<String>,
|
||||
selected: &[String],
|
||||
) -> Vec<String> {
|
||||
let mut merged = Vec::new();
|
||||
let mut remaining = selected.iter();
|
||||
let mut seen = BTreeSet::new();
|
||||
for entry in entries {
|
||||
if !managed.contains(entry) {
|
||||
merged.push(entry.clone());
|
||||
} else if selected.contains(entry)
|
||||
&& seen.insert(entry)
|
||||
&& let Some(next) = remaining.next()
|
||||
{
|
||||
merged.push(next.clone());
|
||||
}
|
||||
}
|
||||
merged.extend(remaining.cloned());
|
||||
merged
|
||||
}
|
||||
|
||||
pub(in crate::api::instance) async fn read_resource_pack_entries(
|
||||
metadata: &InstanceMetadata,
|
||||
state: &State,
|
||||
) -> crate::Result<Option<ResourcePackOptions>> {
|
||||
let path = options_path(metadata, state)?;
|
||||
if !path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let (document, _) = read_document(&path).await?;
|
||||
let Some(raw) = document.value("resourcePacks") else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(ResourcePackOptions {
|
||||
entries: parse_entries(raw)?,
|
||||
incompatible: document
|
||||
.value("incompatibleResourcePacks")
|
||||
.map(parse_entries)
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(in crate::api::instance) async fn merge_resource_pack_entries(
|
||||
metadata: &InstanceMetadata,
|
||||
managed: &BTreeSet<String>,
|
||||
selected: &[String],
|
||||
state: &State,
|
||||
) -> crate::Result<ResourcePackOptionsUpdate> {
|
||||
if sync_files_are_protected(metadata) {
|
||||
return Ok(ResourcePackOptionsUpdate::Deferred);
|
||||
}
|
||||
let path = options_path(metadata, state)?;
|
||||
if !path.exists() {
|
||||
return Ok(ResourcePackOptionsUpdate::Deferred);
|
||||
}
|
||||
let (mut document, input_bytes) = read_document(&path).await?;
|
||||
let Some(raw) = document.value("resourcePacks") else {
|
||||
return Ok(ResourcePackOptionsUpdate::Deferred);
|
||||
};
|
||||
let original = parse_entries(raw)?;
|
||||
let entries = merge_resource_pack_order(&original, managed, selected);
|
||||
let options = ResourcePackOptions {
|
||||
entries,
|
||||
incompatible: document
|
||||
.value("incompatibleResourcePacks")
|
||||
.map(parse_entries)
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
if options.entries == original {
|
||||
return Ok(ResourcePackOptionsUpdate::Applied(options));
|
||||
}
|
||||
if instance_is_running(metadata, state).await? {
|
||||
return Ok(ResourcePackOptionsUpdate::Deferred);
|
||||
}
|
||||
let raw = serde_json::to_string(&options.entries)?;
|
||||
document.set("resourcePacks", &raw, false)?;
|
||||
let output_bytes = document.serialize()?;
|
||||
if output_bytes == input_bytes {
|
||||
return Ok(ResourcePackOptionsUpdate::Applied(options));
|
||||
}
|
||||
io::write(path, output_bytes).await?;
|
||||
Ok(ResourcePackOptionsUpdate::Applied(options))
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
use super::HOTBAR_FILE;
|
||||
use super::files::{
|
||||
CheckpointStatus, begin_checkpoint, checkpoint, ensure_link,
|
||||
finish_checkpoint, instance_game_dir, nbt_from_bytes, nbt_to_bytes,
|
||||
read_nbt_file, safe_instance_id, sha1_bytes, sha1_file,
|
||||
};
|
||||
use super::orchestration::{
|
||||
backup_bytes, create_synced_directories, option_effective,
|
||||
seed_from_instance, synced_options_path,
|
||||
};
|
||||
use crate::state::{InstanceMetadata, SyncedOption};
|
||||
use crate::util::io;
|
||||
use crate::{ErrorKind, State};
|
||||
use quartz_nbt::{NbtCompound, NbtList, NbtTag};
|
||||
use std::path::PathBuf;
|
||||
|
||||
const COMPONENTS_DATA_VERSION_FLOOR: i32 = 3837;
|
||||
pub(super) const HOTBAR_SCHEMA_VERSION: i64 = 2;
|
||||
|
||||
pub(super) struct HotbarState {
|
||||
pub schema_version: i64,
|
||||
pub revision: i64,
|
||||
pub nbt: NbtCompound,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(super) enum HotbarFamily {
|
||||
Legacy,
|
||||
Components,
|
||||
}
|
||||
|
||||
impl HotbarFamily {
|
||||
pub(super) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Legacy => "legacy",
|
||||
Self::Components => "components",
|
||||
}
|
||||
}
|
||||
|
||||
fn other(self) -> Self {
|
||||
match self {
|
||||
Self::Legacy => Self::Components,
|
||||
Self::Components => Self::Legacy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn hotbar_family(
|
||||
metadata: &InstanceMetadata,
|
||||
state: &State,
|
||||
) -> crate::Result<HotbarFamily> {
|
||||
let (manifest, version_index) =
|
||||
crate::launcher::resolve_minecraft_manifest(
|
||||
&metadata.applied_content_set.game_version,
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
let cutoff = manifest
|
||||
.versions
|
||||
.iter()
|
||||
.find(|item| item.id == "1.20.5")
|
||||
.ok_or_else(|| {
|
||||
ErrorKind::LauncherError(
|
||||
"Minecraft 1.20.5 is missing from the version manifest"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok(
|
||||
if manifest.versions[version_index].release_time >= cutoff.release_time
|
||||
{
|
||||
HotbarFamily::Components
|
||||
} else {
|
||||
HotbarFamily::Legacy
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn backup_shared_hotbars(state: &State) -> crate::Result<()> {
|
||||
let sync_state = read_hotbar_state(state).await?;
|
||||
let legacy = hotbar_family_root(&sync_state.nbt, HotbarFamily::Legacy);
|
||||
let components =
|
||||
hotbar_family_root(&sync_state.nbt, HotbarFamily::Components);
|
||||
backup_bytes(
|
||||
"shared",
|
||||
"hotbar-legacy.nbt",
|
||||
&nbt_to_bytes(&legacy)?,
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
backup_bytes(
|
||||
"shared",
|
||||
"hotbar-components.nbt",
|
||||
&nbt_to_bytes(&components)?,
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
backup_bytes(
|
||||
"shared",
|
||||
"hotbars-state.nbt",
|
||||
&nbt_to_bytes(&sync_state.nbt)?,
|
||||
state,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn ensure_hotbar(
|
||||
metadata: &InstanceMetadata,
|
||||
state: &State,
|
||||
) -> crate::Result<()> {
|
||||
create_synced_directories(state).await?;
|
||||
if !hotbar_state_exists(state).await? {
|
||||
seed_from_instance(metadata, SyncedOption::CreativeHotbars, state)
|
||||
.await?;
|
||||
}
|
||||
write_hotbar_projection(metadata, state).await
|
||||
}
|
||||
|
||||
pub(super) async fn reconcile_hotbar(
|
||||
metadata: &InstanceMetadata,
|
||||
state: &State,
|
||||
) -> crate::Result<()> {
|
||||
if !option_effective(metadata, SyncedOption::CreativeHotbars, state).await?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let local = instance_game_dir(metadata, state)?.join(HOTBAR_FILE);
|
||||
if !local.exists() {
|
||||
return ensure_hotbar(metadata, state).await;
|
||||
}
|
||||
let family = hotbar_family(metadata, state).await?;
|
||||
let current_checkpoint = checkpoint(
|
||||
&metadata.instance.id,
|
||||
SyncedOption::CreativeHotbars,
|
||||
family.as_str(),
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
if current_checkpoint
|
||||
.as_ref()
|
||||
.is_some_and(|value| value.status == CheckpointStatus::Pending)
|
||||
{
|
||||
return write_hotbar_projection(metadata, state).await;
|
||||
}
|
||||
let actual = sha1_file(&local).await?;
|
||||
let mut sync_state = read_hotbar_state(state).await?;
|
||||
if current_checkpoint
|
||||
.as_ref()
|
||||
.map(|value| value.expected_sha1.as_str())
|
||||
== Some(actual.as_str())
|
||||
{
|
||||
if current_checkpoint
|
||||
.as_ref()
|
||||
.is_some_and(|value| value.source_revision == sync_state.revision)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
return write_hotbar_projection(metadata, state).await;
|
||||
}
|
||||
|
||||
let (changed_family, checkpoint) = if current_checkpoint.is_none() {
|
||||
let previous_family = family.other();
|
||||
let previous_checkpoint = checkpoint(
|
||||
&metadata.instance.id,
|
||||
SyncedOption::CreativeHotbars,
|
||||
previous_family.as_str(),
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
if let Some(previous_checkpoint) = previous_checkpoint {
|
||||
if previous_checkpoint.status == CheckpointStatus::Pending
|
||||
|| previous_checkpoint.expected_sha1 == actual
|
||||
{
|
||||
return write_hotbar_projection(metadata, state).await;
|
||||
}
|
||||
(previous_family, Some(previous_checkpoint))
|
||||
} else {
|
||||
(family, None)
|
||||
}
|
||||
} else {
|
||||
(family, current_checkpoint)
|
||||
};
|
||||
|
||||
let changed = read_nbt_file(&local).await?;
|
||||
let merge_base = checkpoint
|
||||
.and_then(|value| value.merge_base)
|
||||
.map(nbt_from_bytes)
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| hotbar_family_root(&sync_state.nbt, changed_family));
|
||||
if merge_hotbar_family(
|
||||
&mut sync_state.nbt,
|
||||
changed_family,
|
||||
&merge_base,
|
||||
&changed,
|
||||
) {
|
||||
increment_hotbar_revision(&mut sync_state);
|
||||
write_hotbar_state(state, &sync_state).await?;
|
||||
}
|
||||
regenerate_hotbars(state).await
|
||||
}
|
||||
|
||||
pub(super) async fn instance_hotbars_differ_from_synced(
|
||||
metadata: &InstanceMetadata,
|
||||
state: &State,
|
||||
) -> crate::Result<bool> {
|
||||
let local_path = instance_game_dir(metadata, state)?.join(HOTBAR_FILE);
|
||||
if !local_path.exists() {
|
||||
return Ok(false);
|
||||
}
|
||||
let local = read_nbt_file(&local_path).await?;
|
||||
let family = hotbar_family(metadata, state).await?;
|
||||
let synced =
|
||||
hotbar_family_root(&read_hotbar_state(state).await?.nbt, family);
|
||||
Ok((0..81)
|
||||
.any(|slot| hotbar_slot(&local, slot) != hotbar_slot(&synced, slot)))
|
||||
}
|
||||
|
||||
pub(super) fn merge_hotbar_family(
|
||||
state: &mut NbtCompound,
|
||||
family: HotbarFamily,
|
||||
merge_base: &NbtCompound,
|
||||
changed: &NbtCompound,
|
||||
) -> bool {
|
||||
let family_key = family_state_key(family);
|
||||
let mut current = state
|
||||
.get::<_, &NbtCompound>(family_key)
|
||||
.ok()
|
||||
.cloned()
|
||||
.unwrap_or_else(empty_hotbar_root);
|
||||
let other_family = match family {
|
||||
HotbarFamily::Legacy => HotbarFamily::Components,
|
||||
HotbarFamily::Components => HotbarFamily::Legacy,
|
||||
};
|
||||
let other_key = family_state_key(other_family);
|
||||
let other = state.get::<_, &NbtCompound>(other_key).ok().cloned();
|
||||
let seed_components_with_legacy =
|
||||
other.is_none() && family == HotbarFamily::Legacy;
|
||||
let mut other_root = if seed_components_with_legacy {
|
||||
(*changed).clone()
|
||||
} else {
|
||||
other.unwrap_or_else(empty_hotbar_root)
|
||||
};
|
||||
let mut revisions = state
|
||||
.get::<_, &NbtCompound>("Revisions")
|
||||
.ok()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let family_versions_key = family_data_versions_key(family);
|
||||
let other_versions_key = family_data_versions_key(other_family);
|
||||
let mut family_versions = state
|
||||
.get::<_, &NbtCompound>(family_versions_key)
|
||||
.ok()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut other_versions = state
|
||||
.get::<_, &NbtCompound>(other_versions_key)
|
||||
.ok()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let writer_data_version = hotbar_data_version(changed).max(match family {
|
||||
HotbarFamily::Legacy => 1,
|
||||
HotbarFamily::Components => COMPONENTS_DATA_VERSION_FLOOR,
|
||||
});
|
||||
let current_data_version = hotbar_data_version(¤t);
|
||||
let other_data_version = hotbar_data_version(&other_root);
|
||||
let mut changed_any = false;
|
||||
|
||||
for slot in 0..81 {
|
||||
let old_slot = hotbar_slot(merge_base, slot);
|
||||
let new_slot = hotbar_slot(changed, slot);
|
||||
if old_slot == new_slot {
|
||||
continue;
|
||||
}
|
||||
let current_origin = family_versions
|
||||
.get::<_, i32>(&slot.to_string())
|
||||
.unwrap_or(current_data_version);
|
||||
if writer_data_version > 0
|
||||
&& current_origin > 0
|
||||
&& writer_data_version < current_origin
|
||||
{
|
||||
continue;
|
||||
}
|
||||
set_hotbar_slot_optional(&mut current, slot, new_slot.clone());
|
||||
family_versions.insert(slot.to_string(), writer_data_version);
|
||||
let revision = revisions
|
||||
.get::<_, i64>(&slot.to_string())
|
||||
.unwrap_or(0)
|
||||
.saturating_add(1);
|
||||
revisions.insert(slot.to_string(), revision);
|
||||
if seed_components_with_legacy {
|
||||
other_versions.insert(slot.to_string(), writer_data_version);
|
||||
} else if let Some(slot_value) = new_slot
|
||||
.and_then(|value| convert_hotbar_slot(value, family, other_family))
|
||||
{
|
||||
let other_origin = other_versions
|
||||
.get::<_, i32>(&slot.to_string())
|
||||
.unwrap_or(other_data_version);
|
||||
if writer_data_version <= 0
|
||||
|| other_origin <= 0
|
||||
|| writer_data_version >= other_origin
|
||||
{
|
||||
set_hotbar_slot(&mut other_root, slot, slot_value);
|
||||
other_versions.insert(slot.to_string(), writer_data_version);
|
||||
}
|
||||
}
|
||||
changed_any = true;
|
||||
}
|
||||
|
||||
if writer_data_version > current_data_version {
|
||||
current.insert("DataVersion", writer_data_version);
|
||||
changed_any = true;
|
||||
}
|
||||
if seed_components_with_legacy
|
||||
&& writer_data_version > hotbar_data_version(&other_root)
|
||||
{
|
||||
other_root.insert("DataVersion", writer_data_version);
|
||||
}
|
||||
|
||||
state.insert(family_key, current);
|
||||
state.insert(other_key, other_root);
|
||||
state.insert("Revisions", revisions);
|
||||
state.insert(family_versions_key, family_versions);
|
||||
state.insert(other_versions_key, other_versions);
|
||||
changed_any
|
||||
}
|
||||
|
||||
fn convert_hotbar_slot(
|
||||
value: NbtTag,
|
||||
from: HotbarFamily,
|
||||
to: HotbarFamily,
|
||||
) -> Option<NbtTag> {
|
||||
if from == to {
|
||||
return Some(value);
|
||||
}
|
||||
let NbtTag::Compound(item) = value else {
|
||||
return None;
|
||||
};
|
||||
if item.is_empty() {
|
||||
return Some(NbtCompound::new().into());
|
||||
}
|
||||
let id = item.get::<_, &str>("id").ok()?.to_string();
|
||||
|
||||
match (from, to) {
|
||||
(HotbarFamily::Legacy, HotbarFamily::Components) => {
|
||||
if item.contains_key("tag") {
|
||||
return None;
|
||||
}
|
||||
let count = item
|
||||
.get::<_, i8>("Count")
|
||||
.map(i32::from)
|
||||
.or_else(|_| item.get::<_, i32>("Count"))
|
||||
.unwrap_or(1);
|
||||
let mut converted = NbtCompound::new();
|
||||
converted.insert("id", id);
|
||||
converted.insert("count", count);
|
||||
Some(converted.into())
|
||||
}
|
||||
(HotbarFamily::Components, HotbarFamily::Legacy) => {
|
||||
if item
|
||||
.get::<_, &NbtCompound>("components")
|
||||
.is_ok_and(|components| !components.is_empty())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let count = item.get::<_, i32>("count").unwrap_or(1);
|
||||
let count = i8::try_from(count).ok()?;
|
||||
let mut converted = NbtCompound::new();
|
||||
converted.insert("id", id);
|
||||
converted.insert("Count", count);
|
||||
Some(converted.into())
|
||||
}
|
||||
_ => Some(item.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn hotbar_slot(root: &NbtCompound, slot: usize) -> Option<NbtTag> {
|
||||
let toolbar = (slot / 9).to_string();
|
||||
let position = slot % 9;
|
||||
root.get::<_, &NbtList>(&toolbar)
|
||||
.ok()
|
||||
.and_then(|list| list.as_ref().get(position).cloned())
|
||||
}
|
||||
|
||||
fn set_hotbar_slot(root: &mut NbtCompound, slot: usize, value: NbtTag) {
|
||||
let toolbar = (slot / 9).to_string();
|
||||
let position = slot % 9;
|
||||
let mut list = root
|
||||
.get::<_, &NbtList>(&toolbar)
|
||||
.ok()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
while list.len() < 9 {
|
||||
list.push(NbtCompound::new());
|
||||
}
|
||||
list[position] = value;
|
||||
root.insert(toolbar, list);
|
||||
}
|
||||
|
||||
fn set_hotbar_slot_optional(
|
||||
root: &mut NbtCompound,
|
||||
slot: usize,
|
||||
value: Option<NbtTag>,
|
||||
) {
|
||||
set_hotbar_slot(
|
||||
root,
|
||||
slot,
|
||||
value.unwrap_or_else(|| NbtCompound::new().into()),
|
||||
);
|
||||
}
|
||||
|
||||
fn hotbar_data_version(root: &NbtCompound) -> i32 {
|
||||
root.get::<_, i32>("DataVersion").unwrap_or(0)
|
||||
}
|
||||
|
||||
fn family_data_versions_key(family: HotbarFamily) -> &'static str {
|
||||
match family {
|
||||
HotbarFamily::Legacy => "LegacyDataVersions",
|
||||
HotbarFamily::Components => "ComponentsDataVersions",
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn regenerate_hotbars(state: &State) -> crate::Result<()> {
|
||||
let instances = crate::state::list_instances(&state.pool).await?;
|
||||
for metadata in instances {
|
||||
if option_effective(&metadata, SyncedOption::CreativeHotbars, state)
|
||||
.await?
|
||||
{
|
||||
write_hotbar_projection(&metadata, state).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_hotbar_projection(
|
||||
metadata: &InstanceMetadata,
|
||||
state: &State,
|
||||
) -> crate::Result<()> {
|
||||
let sync_state = read_hotbar_state(state).await?;
|
||||
let family = hotbar_family(metadata, state).await?;
|
||||
let root = hotbar_family_root(&sync_state.nbt, family);
|
||||
let bytes = nbt_to_bytes(&root)?;
|
||||
let expected = sha1_bytes(&bytes);
|
||||
let generated = generated_hotbar_path(state, family, &metadata.instance.id);
|
||||
begin_checkpoint(
|
||||
&metadata.instance.id,
|
||||
SyncedOption::CreativeHotbars,
|
||||
family.as_str(),
|
||||
&expected,
|
||||
Some(&bytes),
|
||||
sync_state.revision,
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
if let Some(parent) = generated.parent() {
|
||||
io::create_dir_all(parent).await?;
|
||||
}
|
||||
io::write(&generated, &bytes).await?;
|
||||
let local = instance_game_dir(metadata, state)?.join(HOTBAR_FILE);
|
||||
let mode = ensure_link(&generated, &local).await?;
|
||||
finish_checkpoint(
|
||||
&metadata.instance.id,
|
||||
SyncedOption::CreativeHotbars,
|
||||
family.as_str(),
|
||||
mode,
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
remove_other_hotbar_checkpoint(&metadata.instance.id, family, state).await
|
||||
}
|
||||
|
||||
async fn remove_other_hotbar_checkpoint(
|
||||
instance_id: &str,
|
||||
family: HotbarFamily,
|
||||
state: &State,
|
||||
) -> crate::Result<()> {
|
||||
let variant = family.as_str();
|
||||
sqlx::query(
|
||||
"DELETE FROM instance_sync_checkpoints
|
||||
WHERE instance_id = ?
|
||||
AND feature = 'creative_hotbars'
|
||||
AND variant != ?",
|
||||
)
|
||||
.bind(instance_id)
|
||||
.bind(variant)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn empty_hotbar_root() -> NbtCompound {
|
||||
let mut root = NbtCompound::new();
|
||||
for toolbar in 0..9 {
|
||||
let mut items = NbtList::new();
|
||||
for _ in 0..9 {
|
||||
items.push(NbtCompound::new());
|
||||
}
|
||||
root.insert(toolbar.to_string(), items);
|
||||
}
|
||||
root
|
||||
}
|
||||
|
||||
pub(super) async fn read_hotbar_state(
|
||||
state: &State,
|
||||
) -> crate::Result<HotbarState> {
|
||||
let row = sqlx::query(
|
||||
"SELECT schema_version, revision, nbt
|
||||
FROM synced_hotbar_state
|
||||
WHERE singleton = 1",
|
||||
)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
if let Some(row) = row {
|
||||
use sqlx::Row;
|
||||
let schema_version: i64 = row.get("schema_version");
|
||||
let revision: i64 = row.get("revision");
|
||||
let nbt: Vec<u8> = row.get("nbt");
|
||||
return Ok(HotbarState {
|
||||
schema_version,
|
||||
revision,
|
||||
nbt: nbt_from_bytes(nbt)?,
|
||||
});
|
||||
}
|
||||
Ok(HotbarState {
|
||||
schema_version: HOTBAR_SCHEMA_VERSION,
|
||||
revision: 0,
|
||||
nbt: NbtCompound::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn hotbar_state_exists(state: &State) -> crate::Result<bool> {
|
||||
Ok(sqlx::query_scalar::<_, bool>(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1 FROM synced_hotbar_state WHERE singleton = 1
|
||||
)",
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub(super) fn hotbar_family_root(
|
||||
state: &NbtCompound,
|
||||
family: HotbarFamily,
|
||||
) -> NbtCompound {
|
||||
let legacy = state
|
||||
.get::<_, &NbtCompound>("Legacy")
|
||||
.ok()
|
||||
.cloned()
|
||||
.unwrap_or_else(empty_hotbar_root);
|
||||
match family {
|
||||
HotbarFamily::Legacy => legacy,
|
||||
HotbarFamily::Components => state
|
||||
.get::<_, &NbtCompound>("Components")
|
||||
.ok()
|
||||
.cloned()
|
||||
.unwrap_or(legacy),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn increment_hotbar_revision(state: &mut HotbarState) {
|
||||
state.schema_version = HOTBAR_SCHEMA_VERSION;
|
||||
state.revision = state.revision.saturating_add(1);
|
||||
}
|
||||
|
||||
pub(super) async fn write_hotbar_state(
|
||||
state: &State,
|
||||
hotbar_state: &HotbarState,
|
||||
) -> crate::Result<()> {
|
||||
let bytes = nbt_to_bytes(&hotbar_state.nbt)?;
|
||||
sqlx::query(
|
||||
"INSERT INTO synced_hotbar_state
|
||||
(singleton, schema_version, revision, nbt)
|
||||
VALUES (1, ?, ?, ?)
|
||||
ON CONFLICT(singleton) DO UPDATE SET
|
||||
schema_version = excluded.schema_version,
|
||||
revision = excluded.revision,
|
||||
nbt = excluded.nbt",
|
||||
)
|
||||
.bind(hotbar_state.schema_version)
|
||||
.bind(hotbar_state.revision)
|
||||
.bind(bytes)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn family_state_key(family: HotbarFamily) -> &'static str {
|
||||
match family {
|
||||
HotbarFamily::Legacy => "Legacy",
|
||||
HotbarFamily::Components => "Components",
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn generated_hotbar_path(
|
||||
state: &State,
|
||||
family: HotbarFamily,
|
||||
instance_id: &str,
|
||||
) -> PathBuf {
|
||||
synced_options_path(state)
|
||||
.join("hotbars/generated")
|
||||
.join(family.as_str())
|
||||
.join(safe_instance_id(instance_id))
|
||||
.join(HOTBAR_FILE)
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
mod command_history;
|
||||
mod files;
|
||||
mod game_options;
|
||||
pub(crate) mod game_options;
|
||||
mod hotbars;
|
||||
mod orchestration;
|
||||
pub(in crate::api::instance) mod pending;
|
||||
|
||||
const COMMAND_HISTORY_FILE: &str = "command_history.txt";
|
||||
const HOTBAR_FILE: &str = "hotbar.nbt";
|
||||
|
||||
pub use self::command_history::{get_command_history, set_command_history};
|
||||
pub(super) use self::files::{
|
||||
CheckpointStatus, begin_checkpoint, checkpoint, detach_link, ensure_link,
|
||||
finish_checkpoint, finish_plain_checkpoint, instance_game_dir,
|
||||
@@ -12,3 +16,17 @@ pub(super) use self::files::{
|
||||
option_can_apply_while_running, read_nbt_file, safe_instance_id,
|
||||
sha1_bytes, sha1_file, sync_files_are_protected, synced_options_path,
|
||||
};
|
||||
pub(super) use self::orchestration::instance_option_supported;
|
||||
pub use self::orchestration::{
|
||||
GlobalSyncedOptions, SyncedOptionCapability, SyncedOptionJoinAction,
|
||||
SyncedOptionJoinPreview, SyncedOptionJoinResolution, SyncedOptionsOverview,
|
||||
get_capabilities, get_global_options, get_initialized_options,
|
||||
get_instance_option_join_preview, get_overview, get_synced_options_folder,
|
||||
reconcile_all, reconcile_changed_file, reconcile_instance,
|
||||
set_global_option, set_instance_option,
|
||||
};
|
||||
pub(crate) use self::orchestration::{
|
||||
monitor_persisted_processes, prepare_instance_update,
|
||||
reconcile_instance_after_pack_update_with_state,
|
||||
remove_generated_instance_files,
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1835,6 +1835,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn set_instance_sync_preference(
|
||||
instance_id: &str,
|
||||
option: crate::state::SyncedOption,
|
||||
enabled: bool,
|
||||
pool: &SqlitePool,
|
||||
) -> crate::Result<()> {
|
||||
sqlx::query(
|
||||
"INSERT INTO instance_sync_preferences (instance_id, feature, enabled)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(instance_id, feature) DO UPDATE SET enabled = excluded.enabled",
|
||||
)
|
||||
.bind(instance_id)
|
||||
.bind(option.as_str())
|
||||
.bind(enabled)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_instance_synced_options(
|
||||
instance_id: &str,
|
||||
pool: &SqlitePool,
|
||||
|
||||
Reference in new issue
Block a user