feat: 跨实例同步移植 WIP 检查点(第1轮+2a+2b+synced_servers)
Sync source to CNB / Sync Git ref (push) Has been cancelled

从 Axolotl 移植跨实例同步子系统,当前进度:

- 第1轮:synced_options/files.rs + mod.rs 骨架,state 层补齐 SyncedOption/InstanceSyncedOptions/CanonicalValue/GameOptionKind 等

- 2a:game_options 的 api_types/options_file/catalog(10文件)

- 2b:game_options 除 resource_pack_options/local_settings_editor 外的全部(settings_editor/write_shared_settings/fullscreen/launch_overrides/instance_support/read_instance_changes/pack_updates/source_selection + locales/5文件);State 补 synced_options_lock/game_locale_indexer/lock_synced_options

- synced_servers:6 文件全部搬入

- 35+20 处 sqlx::query! 宏改写为非宏 query+bind

已知未完成:synced_servers::operations 依赖 synced_options::instance_option_supported(属 orchestration,第5轮),当前编译不通。
This commit is contained in:
AxTps committed 2026-10-03 22:18:37 +08:00
1 parent 5a289c3a15
commit ed61a5942c
43 files changed
+10607

No files matched your search

@@ -0,0 +1,104 @@
use super::files::synced_options_path;
use crate::State;
use crate::state::SyncedOption;
use crate::util::io;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SyncedOptionJoinResolution {
UseSynced,
UseInstance,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub(super) enum PendingAction {
SelectSource,
Join {
resolution: Option<SyncedOptionJoinResolution>,
},
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub(super) struct PendingChange {
pub instance_id: String,
pub option: SyncedOption,
pub action: PendingAction,
}
pub(super) async fn read(state: &State) -> crate::Result<Vec<PendingChange>> {
let path = synced_options_path(state).join("pending-changes.json");
match tokio::fs::read(path).await {
Ok(bytes) => Ok(serde_json::from_slice(&bytes)?),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
Ok(Vec::new())
}
Err(error) => Err(error.into()),
}
}
async fn write(changes: &[PendingChange], state: &State) -> crate::Result<()> {
let directory = synced_options_path(state);
io::create_dir_all(&directory).await?;
let temporary = directory.join("pending-changes.json.tmp");
io::write(&temporary, serde_json::to_vec(changes)?).await?;
tokio::fs::rename(temporary, directory.join("pending-changes.json"))
.await?;
Ok(())
}
/// Callers hold the synced-options lock while updating or replaying requests.
pub(super) async fn save(
change: PendingChange,
state: &State,
) -> crate::Result<()> {
let mut changes = read(state).await?;
changes.retain(|previous| {
previous.option != change.option
|| (change.action != PendingAction::SelectSource
&& previous.instance_id != change.instance_id)
});
changes.push(change);
write(&changes, state).await
}
pub(super) async fn remove(
change: &PendingChange,
state: &State,
) -> crate::Result<()> {
let mut changes = read(state).await?;
let previous_len = changes.len();
changes.retain(|previous| previous != change);
if changes.len() != previous_len {
write(&changes, state).await?;
}
Ok(())
}
pub(super) async fn cancel(
option: SyncedOption,
instance_id: Option<&str>,
state: &State,
) -> crate::Result<()> {
let mut changes = read(state).await?;
let previous_len = changes.len();
changes.retain(|change| {
change.option != option
|| instance_id.is_some_and(|id| change.instance_id != id)
});
if changes.len() != previous_len {
write(&changes, state).await?;
}
Ok(())
}
pub(in crate::api::instance) async fn contains(
instance_id: &str,
option: SyncedOption,
state: &State,
) -> crate::Result<bool> {
Ok(read(state).await?.iter().any(|change| {
change.instance_id == instance_id && change.option == option
}))
}