feat(sync): 跨实例同步第1轮——同步功能表、InstanceSyncedOptions 类型与实例元数据字段
Sync source to CNB / Sync Git ref (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
- 新增 4 个 migration:同步功能表/instance_servers 扩展/synced_pack_selection/禁用发现选项 - 新增 InstanceSyncedOptions 类型(per-instance 同步开关) - InstanceMetadata 增加 synced_options 字段并在 3 个查询点填充 - instance_rows 新增 get_instance_synced_options - 前端 GameInstance 增加 synced_options 类型
This commit is contained in:
1 parent
91e63fdb2c
commit
5a289c3a15
10 files changed
+302
-6
No files matched your search
+11
@@ -23,6 +23,17 @@ export type GameInstance = {
|
|||||||
loader_version?: string
|
loader_version?: string
|
||||||
loader_components: LoaderComponent[]
|
loader_components: LoaderComponent[]
|
||||||
|
|
||||||
|
synced_options: Record<
|
||||||
|
| 'game_options'
|
||||||
|
| 'command_history'
|
||||||
|
| 'multiplayer_servers'
|
||||||
|
| 'creative_hotbars'
|
||||||
|
| 'screenshots'
|
||||||
|
| 'resource_packs'
|
||||||
|
| 'data_packs',
|
||||||
|
boolean
|
||||||
|
>
|
||||||
|
|
||||||
groups: string[]
|
groups: string[]
|
||||||
|
|
||||||
link?: InstanceLink | null
|
link?: InstanceLink | null
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
-- Instance settings synchronization: shared feature tables.
|
||||||
|
--
|
||||||
|
-- Split out of Axolotl's combined "instance-sync-and-screenshot-center"
|
||||||
|
-- migration so that the screenshot-center tables (already present in this
|
||||||
|
-- fork under its own migration) are not created twice.
|
||||||
|
|
||||||
|
CREATE TABLE sync_feature_settings (
|
||||||
|
feature TEXT PRIMARY KEY NOT NULL,
|
||||||
|
globally_enabled INTEGER NOT NULL CHECK (globally_enabled IN (0, 1)),
|
||||||
|
new_instance_default INTEGER NOT NULL CHECK (new_instance_default IN (0, 1))
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO sync_feature_settings (feature, globally_enabled, new_instance_default)
|
||||||
|
VALUES
|
||||||
|
('command_history', 0, 0),
|
||||||
|
('multiplayer_servers', 0, 0),
|
||||||
|
('creative_hotbars', 0, 0),
|
||||||
|
('screenshots', 0, 0),
|
||||||
|
('game_options', 0, 0),
|
||||||
|
('resource_packs', 0, 0),
|
||||||
|
('data_packs', 0, 0);
|
||||||
|
|
||||||
|
CREATE TABLE instance_sync_preferences (
|
||||||
|
instance_id TEXT NOT NULL,
|
||||||
|
feature TEXT NOT NULL,
|
||||||
|
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
|
||||||
|
PRIMARY KEY (instance_id, feature),
|
||||||
|
FOREIGN KEY (instance_id) REFERENCES instances (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (feature) REFERENCES sync_feature_settings (feature) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX instance_sync_preferences_feature_enabled
|
||||||
|
ON instance_sync_preferences (feature, enabled);
|
||||||
|
|
||||||
|
INSERT INTO instance_sync_preferences (instance_id, feature, enabled)
|
||||||
|
SELECT instances.id, features.feature, features.enabled
|
||||||
|
FROM instances
|
||||||
|
CROSS JOIN (
|
||||||
|
SELECT 'command_history' AS feature, 0 AS enabled
|
||||||
|
UNION ALL SELECT 'multiplayer_servers', 0
|
||||||
|
UNION ALL SELECT 'creative_hotbars', 0
|
||||||
|
UNION ALL SELECT 'screenshots', 0
|
||||||
|
) AS features;
|
||||||
|
|
||||||
|
INSERT INTO instance_sync_preferences (instance_id, feature, enabled)
|
||||||
|
SELECT id, 'game_options', 0 FROM instances;
|
||||||
|
|
||||||
|
INSERT INTO instance_sync_preferences (instance_id, feature, enabled)
|
||||||
|
SELECT instances.id, features.feature, 0
|
||||||
|
FROM instances
|
||||||
|
CROSS JOIN (
|
||||||
|
SELECT 'resource_packs' AS feature
|
||||||
|
UNION ALL SELECT 'data_packs'
|
||||||
|
) AS features;
|
||||||
|
|
||||||
|
CREATE TABLE synced_game_option_state (
|
||||||
|
singleton INTEGER PRIMARY KEY NOT NULL CHECK (singleton = 1),
|
||||||
|
revision INTEGER NOT NULL CHECK (revision >= 0),
|
||||||
|
catalog_revision INTEGER NOT NULL CHECK (catalog_revision >= 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE synced_game_option_values (
|
||||||
|
option_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
kind TEXT NOT NULL CHECK (kind IN ('vanilla', 'external')),
|
||||||
|
raw_key TEXT,
|
||||||
|
canonical_type TEXT NOT NULL,
|
||||||
|
canonical_value_json TEXT,
|
||||||
|
value_codec TEXT NOT NULL,
|
||||||
|
seeded INTEGER NOT NULL CHECK (seeded IN (0, 1)),
|
||||||
|
revision INTEGER NOT NULL CHECK (revision >= 0),
|
||||||
|
origin TEXT NOT NULL CHECK (origin IN ('app_editor', 'instance', 'source_seed')),
|
||||||
|
source_game_version TEXT,
|
||||||
|
source_instance_id TEXT,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE synced_game_option_preferences (
|
||||||
|
option_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
|
||||||
|
source TEXT NOT NULL CHECK (source IN ('catalog_default', 'discovery_default', 'user')),
|
||||||
|
revision INTEGER NOT NULL CHECK (revision >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE instance_game_option_pack_bases (
|
||||||
|
instance_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
pack_version_id TEXT,
|
||||||
|
source TEXT NOT NULL CHECK (source IN ('client_overrides', 'overrides', 'none')),
|
||||||
|
sha1 TEXT,
|
||||||
|
encoding TEXT,
|
||||||
|
document BLOB,
|
||||||
|
FOREIGN KEY (instance_id) REFERENCES instances (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE instance_game_option_update_state (
|
||||||
|
instance_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
had_file INTEGER NOT NULL CHECK (had_file IN (0, 1)),
|
||||||
|
sha1 TEXT,
|
||||||
|
document BLOB,
|
||||||
|
FOREIGN KEY (instance_id) REFERENCES instances (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE game_option_locale_origins (
|
||||||
|
scope TEXT NOT NULL,
|
||||||
|
option_id TEXT NOT NULL,
|
||||||
|
source_instance_id TEXT,
|
||||||
|
source_game_version TEXT,
|
||||||
|
backfilled INTEGER NOT NULL DEFAULT 0,
|
||||||
|
observation_json TEXT,
|
||||||
|
origin_json TEXT,
|
||||||
|
PRIMARY KEY (scope, option_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO game_option_locale_origins
|
||||||
|
(scope, option_id, source_instance_id, source_game_version, backfilled)
|
||||||
|
SELECT '', option_id, source_instance_id, source_game_version, 1
|
||||||
|
FROM synced_game_option_values;
|
||||||
|
|
||||||
|
CREATE TRIGGER record_game_option_locale_origin
|
||||||
|
AFTER INSERT ON synced_game_option_values
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO game_option_locale_origins
|
||||||
|
(scope, option_id, source_instance_id, source_game_version)
|
||||||
|
VALUES ('', NEW.option_id, NEW.source_instance_id, NEW.source_game_version)
|
||||||
|
ON CONFLICT(scope, option_id) DO NOTHING;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TABLE instance_sync_checkpoints (
|
||||||
|
instance_id TEXT NOT NULL,
|
||||||
|
feature TEXT NOT NULL,
|
||||||
|
variant TEXT NOT NULL CHECK (variant IN ('default', 'legacy', 'components')),
|
||||||
|
expected_sha1 TEXT NOT NULL,
|
||||||
|
merge_base BLOB,
|
||||||
|
source_revision INTEGER NOT NULL CHECK (source_revision >= 0),
|
||||||
|
status TEXT NOT NULL CHECK (status IN ('pending', 'ready')),
|
||||||
|
link_mode TEXT CHECK (link_mode IS NULL OR link_mode IN ('copy', 'hard', 'symbolic')),
|
||||||
|
PRIMARY KEY (instance_id, feature, variant),
|
||||||
|
FOREIGN KEY (instance_id, feature)
|
||||||
|
REFERENCES instance_sync_preferences (instance_id, feature) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE synced_hotbar_state (
|
||||||
|
singleton INTEGER PRIMARY KEY NOT NULL CHECK (singleton = 1),
|
||||||
|
schema_version INTEGER NOT NULL CHECK (schema_version >= 1),
|
||||||
|
revision INTEGER NOT NULL CHECK (revision >= 0),
|
||||||
|
nbt BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE synced_server_state (
|
||||||
|
singleton INTEGER PRIMARY KEY NOT NULL CHECK (singleton = 1),
|
||||||
|
revision INTEGER NOT NULL CHECK (revision >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE synced_servers (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
position INTEGER NOT NULL UNIQUE CHECK (position >= 0),
|
||||||
|
nbt BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE instance_servers (
|
||||||
|
instance_id TEXT NOT NULL,
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
source TEXT NOT NULL CHECK (source IN ('modpack', 'local_desynced')),
|
||||||
|
excluded_synced_server_id TEXT,
|
||||||
|
nbt BLOB NOT NULL,
|
||||||
|
position INTEGER NOT NULL CHECK (position >= 0),
|
||||||
|
PRIMARY KEY (instance_id, id),
|
||||||
|
UNIQUE (instance_id, position),
|
||||||
|
FOREIGN KEY (instance_id) REFERENCES instances (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (excluded_synced_server_id) REFERENCES synced_servers (id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE instance_server_projection_entries (
|
||||||
|
instance_id TEXT NOT NULL,
|
||||||
|
owner TEXT NOT NULL CHECK (owner IN ('synced', 'instance')),
|
||||||
|
server_id TEXT NOT NULL,
|
||||||
|
nbt BLOB NOT NULL,
|
||||||
|
position INTEGER NOT NULL CHECK (position >= 0),
|
||||||
|
PRIMARY KEY (instance_id, owner, server_id),
|
||||||
|
UNIQUE (instance_id, position),
|
||||||
|
FOREIGN KEY (instance_id) REFERENCES instances (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE instance_server_pack_state (
|
||||||
|
instance_id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
version_id TEXT,
|
||||||
|
FOREIGN KEY (instance_id) REFERENCES instances (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE settings ADD COLUMN sync_features_across_devices INTEGER NOT NULL DEFAULT FALSE;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
-- Axolotl stores managed server-project entries alongside the local additions
|
||||||
|
-- that are composed with the launcher-wide multiplayer server list. Extend
|
||||||
|
-- the original sync table constraint without changing the already-applied
|
||||||
|
-- instance-sync migration.
|
||||||
|
CREATE TABLE instance_servers_new (
|
||||||
|
instance_id TEXT NOT NULL,
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
source TEXT NOT NULL CHECK (
|
||||||
|
source IN ('modpack', 'linked_server_project', 'local_desynced')
|
||||||
|
),
|
||||||
|
excluded_synced_server_id TEXT,
|
||||||
|
nbt BLOB NOT NULL,
|
||||||
|
position INTEGER NOT NULL CHECK (position >= 0),
|
||||||
|
PRIMARY KEY (instance_id, id),
|
||||||
|
UNIQUE (instance_id, position),
|
||||||
|
FOREIGN KEY (instance_id) REFERENCES instances (id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (excluded_synced_server_id)
|
||||||
|
REFERENCES synced_servers (id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO instance_servers_new
|
||||||
|
(instance_id, id, source, excluded_synced_server_id, nbt, position)
|
||||||
|
SELECT instance_id, id, source, excluded_synced_server_id, nbt, position
|
||||||
|
FROM instance_servers;
|
||||||
|
|
||||||
|
DROP TABLE instance_servers;
|
||||||
|
ALTER TABLE instance_servers_new RENAME TO instance_servers;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE synced_pack_catalog ADD COLUMN selected INTEGER NOT NULL DEFAULT 1 CHECK (selected IN (0, 1));
|
||||||
|
ALTER TABLE synced_pack_catalog ADD COLUMN selection_order INTEGER;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- Discovered options are catalogued for review, but must not be synced until
|
||||||
|
-- the user explicitly enables them.
|
||||||
|
UPDATE synced_game_option_preferences
|
||||||
|
SET enabled = 0
|
||||||
|
WHERE source = 'discovery_default' AND enabled = 1;
|
||||||
@@ -394,6 +394,7 @@ mod tests {
|
|||||||
groups: Vec::new(),
|
groups: Vec::new(),
|
||||||
launch_overrides: InstanceLaunchOverrides::empty(instance_id),
|
launch_overrides: InstanceLaunchOverrides::empty(instance_id),
|
||||||
loader_components: Vec::new(),
|
loader_components: Vec::new(),
|
||||||
|
synced_options: Default::default(),
|
||||||
},
|
},
|
||||||
install_stage: InstanceInstallStage::Installed,
|
install_stage: InstanceInstallStage::Installed,
|
||||||
content: None,
|
content: None,
|
||||||
|
|||||||
@@ -1834,3 +1834,31 @@ mod tests {
|
|||||||
assert_eq!(launch.applied_content_set.revision, 5);
|
assert_eq!(launch.applied_content_set.revision, 5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_instance_synced_options(
|
||||||
|
instance_id: &str,
|
||||||
|
pool: &SqlitePool,
|
||||||
|
) -> crate::Result<crate::state::InstanceSyncedOptions> {
|
||||||
|
let rows = sqlx::query(
|
||||||
|
"SELECT feature, enabled FROM instance_sync_preferences WHERE instance_id = ?",
|
||||||
|
)
|
||||||
|
.bind(instance_id)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
let mut options = crate::state::InstanceSyncedOptions::default();
|
||||||
|
for row in rows {
|
||||||
|
let feature: String = sqlx::Row::get(&row, "feature");
|
||||||
|
let enabled: bool = sqlx::Row::get(&row, "enabled");
|
||||||
|
match feature.as_str() {
|
||||||
|
"game_options" => options.game_options = enabled,
|
||||||
|
"command_history" => options.command_history = enabled,
|
||||||
|
"multiplayer_servers" => options.multiplayer_servers = enabled,
|
||||||
|
"creative_hotbars" => options.creative_hotbars = enabled,
|
||||||
|
"screenshots" => options.screenshots = enabled,
|
||||||
|
"resource_packs" => options.resource_packs = enabled,
|
||||||
|
"data_packs" => options.data_packs = enabled,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(options)
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ pub struct InstanceMetadata {
|
|||||||
pub launch_overrides: InstanceLaunchOverrides,
|
pub launch_overrides: InstanceLaunchOverrides,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub loader_components: Vec<LoaderComponent>,
|
pub loader_components: Vec<LoaderComponent>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub synced_options: crate::state::InstanceSyncedOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_instance(
|
pub(crate) async fn get_instance(
|
||||||
@@ -36,10 +38,10 @@ pub(crate) async fn get_instance_metadata(
|
|||||||
let loader_components =
|
let loader_components =
|
||||||
loader_component_rows::list_loader_components(instance_id, pool)
|
loader_component_rows::list_loader_components(instance_id, pool)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Some(InstanceMetadata::from_record(
|
let mut metadata = InstanceMetadata::from_record(record, loader_components);
|
||||||
record,
|
metadata.synced_options =
|
||||||
loader_components,
|
instance_rows::get_instance_synced_options(instance_id, pool).await?;
|
||||||
)))
|
Ok(Some(metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_instances_metadata(
|
pub(crate) async fn get_instances_metadata(
|
||||||
@@ -55,7 +57,11 @@ pub(crate) async fn get_instances_metadata(
|
|||||||
pool,
|
pool,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
metadata.push(InstanceMetadata::from_record(record, components));
|
let mut item = InstanceMetadata::from_record(record, components);
|
||||||
|
item.synced_options =
|
||||||
|
instance_rows::get_instance_synced_options(&item.instance.id, pool)
|
||||||
|
.await?;
|
||||||
|
metadata.push(item);
|
||||||
}
|
}
|
||||||
Ok(metadata)
|
Ok(metadata)
|
||||||
}
|
}
|
||||||
@@ -71,7 +77,11 @@ pub(crate) async fn list_instances(
|
|||||||
pool,
|
pool,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
metadata.push(InstanceMetadata::from_record(record, components));
|
let mut item = InstanceMetadata::from_record(record, components);
|
||||||
|
item.synced_options =
|
||||||
|
instance_rows::get_instance_synced_options(&item.instance.id, pool)
|
||||||
|
.await?;
|
||||||
|
metadata.push(item);
|
||||||
}
|
}
|
||||||
Ok(metadata)
|
Ok(metadata)
|
||||||
}
|
}
|
||||||
@@ -88,6 +98,7 @@ impl InstanceMetadata {
|
|||||||
groups: record.groups,
|
groups: record.groups,
|
||||||
launch_overrides: record.launch_overrides,
|
launch_overrides: record.launch_overrides,
|
||||||
loader_components,
|
loader_components,
|
||||||
|
synced_options: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,6 +468,7 @@ mod tests {
|
|||||||
groups: vec!["Group A".to_string()],
|
groups: vec!["Group A".to_string()],
|
||||||
launch_overrides,
|
launch_overrides,
|
||||||
loader_components: Vec::new(),
|
loader_components: Vec::new(),
|
||||||
|
synced_options: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,3 +88,24 @@ pub(crate) fn playtime_to_storage(
|
|||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-instance synchronization feature switches. Mirrors the Axolotl
|
||||||
|
/// `InstanceSyncedOptions` shape so the sync subsystem can be ported on top of
|
||||||
|
/// the shared `instance_sync_preferences` table.
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
pub struct InstanceSyncedOptions {
|
||||||
|
#[serde(default)]
|
||||||
|
pub game_options: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub command_history: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub multiplayer_servers: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub creative_hotbars: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub screenshots: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub resource_packs: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub data_packs: bool,
|
||||||
|
}
|
||||||
Reference in new issue
Block a user