feat: 跨实例同步移植 local_settings_editor(2c 收尾)
Sync source to CNB / Sync Git ref (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
- local_settings_editor.rs:单实例(未同步)游戏设置的读取/预览/保存 - game_options/mod.rs 补声明与 re-export - api/instance/mod.rs 补 game_options 的公开导出(get_local_game_options_config 等)
This commit is contained in:
1 parent
b7c16594ce
commit
121da431ed
3 files changed
+435
No files matched your search
@@ -123,6 +123,18 @@ pub use self::screenshots::{
|
||||
list_all_screenshots, list_screenshots, list_synced_screenshots,
|
||||
move_screenshots, save_edited_screenshot,
|
||||
};
|
||||
pub use self::synced_options::game_options::{
|
||||
GameOptionsSourceCandidate, GameSettingLocaleLabels,
|
||||
GameSettingsEditorState, SaveGameSettingsResult, UpdateGameSettingsRequest,
|
||||
get_config as get_synced_game_options_config,
|
||||
get_game_setting_locale_labels,
|
||||
get_local_config as get_local_game_options_config,
|
||||
list_sync_sources as list_game_options_sync_sources,
|
||||
preview_changes as preview_synced_game_option_changes,
|
||||
preview_local_changes as preview_local_game_option_changes,
|
||||
save_changes as save_synced_game_option_changes,
|
||||
save_local_changes as save_local_game_option_changes,
|
||||
};
|
||||
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,
|
||||
|
||||
+418
@@ -0,0 +1,418 @@
|
||||
//! Loads and saves the game settings stored by one unsynced instance.
|
||||
|
||||
use super::super as synced_options;
|
||||
use super::CATALOG_REVISION;
|
||||
use super::api_types::*;
|
||||
use super::catalog::*;
|
||||
use super::launch_overrides::currently_launcher_owned_keys;
|
||||
use super::options_file::{
|
||||
GameOptionsDocument, input_error, options_path, read_document, sha1_bytes,
|
||||
validate_raw_key_value,
|
||||
};
|
||||
use super::read_instance_changes::custom_setting_id;
|
||||
use crate::state::{CanonicalValue, GameOptionKind, InstanceMetadata, State};
|
||||
use crate::util::io;
|
||||
use std::collections::HashSet;
|
||||
|
||||
fn compatibility(controlled: bool) -> GameOptionCompatibility {
|
||||
if controlled {
|
||||
GameOptionCompatibility {
|
||||
total_participating: 1,
|
||||
will_receive: 0,
|
||||
write_now: 0,
|
||||
left_local: 1,
|
||||
buckets: vec![GameOptionCompatibilityBucket {
|
||||
instance_count: 1,
|
||||
write_keys: Vec::new(),
|
||||
eventual_keys: Vec::new(),
|
||||
game_versions: Vec::new(),
|
||||
status: GameOptionCompatibilityStatus::Controlled,
|
||||
mapping: None,
|
||||
reason: Some(GameOptionCompatibilityReason::LauncherControlled),
|
||||
}],
|
||||
}
|
||||
} else {
|
||||
GameOptionCompatibility {
|
||||
total_participating: 1,
|
||||
will_receive: 1,
|
||||
write_now: 1,
|
||||
left_local: 0,
|
||||
buckets: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn categories(settings: &[EditableGameSetting]) -> Vec<GameSettingCategory> {
|
||||
[
|
||||
"skin_customization",
|
||||
"video",
|
||||
"language",
|
||||
"music_and_sound",
|
||||
"controls",
|
||||
"chat",
|
||||
"accessibility",
|
||||
"online",
|
||||
"custom",
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|id| settings.iter().any(|setting| setting.category_id == *id))
|
||||
.map(|id| GameSettingCategory {
|
||||
id: id.to_string(),
|
||||
is_custom: id == "custom",
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn editor_revision(metadata: &InstanceMetadata, input_bytes: &[u8]) -> String {
|
||||
sha1_bytes(
|
||||
format!(
|
||||
"{CATALOG_REVISION}:{}:{}",
|
||||
metadata.applied_content_set.game_version,
|
||||
sha1_bytes(input_bytes)
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn load_instance_document(
|
||||
instance_id: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<(
|
||||
InstanceMetadata,
|
||||
GameOptionsDocument,
|
||||
Vec<u8>,
|
||||
HashSet<String>,
|
||||
)> {
|
||||
let metadata = crate::state::get_instance(instance_id, &state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| input_error("Unknown instance"))?;
|
||||
if crate::state::game_options_sync_is_enabled(&state.pool).await?
|
||||
&& metadata.synced_options.game_options
|
||||
{
|
||||
return Err(input_error(
|
||||
"This instance is using synced game settings.",
|
||||
));
|
||||
}
|
||||
if synced_options::sync_files_are_protected(&metadata) {
|
||||
return Err(input_error(
|
||||
"Game settings cannot be edited while this instance is installing or updating.",
|
||||
));
|
||||
}
|
||||
let path = options_path(&metadata, state)?;
|
||||
if !path.exists() {
|
||||
return Err(input_error(
|
||||
"Launch this instance once so Minecraft can create options.txt.",
|
||||
));
|
||||
}
|
||||
let (document, input_bytes) = read_document(&path).await?;
|
||||
let mut controlled_keys = currently_launcher_owned_keys(&metadata);
|
||||
if metadata.launch_overrides.force_fullscreen.is_none()
|
||||
&& crate::state::Settings::get(&state.pool)
|
||||
.await?
|
||||
.force_fullscreen
|
||||
{
|
||||
controlled_keys.insert("fullscreen".to_string());
|
||||
}
|
||||
Ok((metadata, document, input_bytes, controlled_keys))
|
||||
}
|
||||
|
||||
fn build_editor_state(
|
||||
metadata: &InstanceMetadata,
|
||||
document: &GameOptionsDocument,
|
||||
input_bytes: &[u8],
|
||||
controlled_keys: &HashSet<String>,
|
||||
) -> GameSettingsEditorState {
|
||||
let game_version = metadata.applied_content_set.game_version.as_str();
|
||||
let mut settings = Vec::new();
|
||||
|
||||
if supported_settings_cover_game_version(game_version) {
|
||||
for definition in all_supported_settings() {
|
||||
let Some(physical_key) =
|
||||
observed_physical_key(definition, document, game_version)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(value) = document.value(&physical_key).and_then(|raw| {
|
||||
decode_value_for_version(
|
||||
definition,
|
||||
&physical_key,
|
||||
raw,
|
||||
game_version,
|
||||
)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let controlled = controlled_keys.contains(definition.id);
|
||||
settings.push(EditableGameSetting {
|
||||
option_id: definition.id.to_string(),
|
||||
category_id: definition.category.to_string(),
|
||||
kind: GameOptionKind::Vanilla,
|
||||
raw_key: None,
|
||||
sync_enabled: true,
|
||||
canonical_value: Some(value),
|
||||
value_state: GameOptionValueState::Canonical,
|
||||
option_revision: 0,
|
||||
editor: editor_for(definition),
|
||||
compatibility: compatibility(controlled),
|
||||
validation_error: None,
|
||||
controlled,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut custom_settings = document
|
||||
.effective_entries()
|
||||
.into_iter()
|
||||
.filter_map(|(key, (index, _))| {
|
||||
if controlled_keys.contains(key)
|
||||
|| setting_by_file_key(key).is_some()
|
||||
|| is_never_sync_key(key)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let entry = document.lines[index].entry.as_ref()?;
|
||||
validate_raw_key_value(key, &entry.value).ok()?;
|
||||
Some(EditableGameSetting {
|
||||
option_id: custom_setting_id(key),
|
||||
category_id: "custom".to_string(),
|
||||
kind: GameOptionKind::External,
|
||||
raw_key: Some(key.to_string()),
|
||||
sync_enabled: true,
|
||||
canonical_value: Some(CanonicalValue::ExternalRaw(
|
||||
entry.value.clone(),
|
||||
)),
|
||||
value_state: GameOptionValueState::Canonical,
|
||||
option_revision: 0,
|
||||
editor: custom_setting_editor(),
|
||||
compatibility: compatibility(false),
|
||||
validation_error: None,
|
||||
controlled: false,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
custom_settings.sort_by(|left, right| left.raw_key.cmp(&right.raw_key));
|
||||
settings.extend(custom_settings);
|
||||
|
||||
GameSettingsEditorState {
|
||||
summary_revision: editor_revision(metadata, input_bytes),
|
||||
canonical_revision: 0,
|
||||
catalog_revision: CATALOG_REVISION,
|
||||
total_participating: 1,
|
||||
categories: categories(&settings),
|
||||
settings,
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_setting_value(
|
||||
metadata: &InstanceMetadata,
|
||||
document: &GameOptionsDocument,
|
||||
setting: &EditableGameSetting,
|
||||
value: &CanonicalValue,
|
||||
) -> crate::Result<(String, String)> {
|
||||
let game_version = metadata.applied_content_set.game_version.as_str();
|
||||
if let Some(definition) = setting_by_id(&setting.option_id) {
|
||||
validate_canonical_value(Some(definition), value)?;
|
||||
let key = observed_physical_key(definition, document, game_version)
|
||||
.ok_or_else(|| {
|
||||
input_error(format!(
|
||||
"{} is no longer present in options.txt",
|
||||
setting.option_id
|
||||
))
|
||||
})?;
|
||||
let raw = encode_value(
|
||||
definition,
|
||||
&key,
|
||||
value,
|
||||
game_version,
|
||||
document.value(&key),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
input_error(format!(
|
||||
"{} cannot be represented by this Minecraft version",
|
||||
setting.option_id
|
||||
))
|
||||
})?;
|
||||
Ok((key, raw))
|
||||
} else {
|
||||
validate_canonical_value(None, value)?;
|
||||
let key = setting.raw_key.clone().ok_or_else(|| {
|
||||
input_error("Custom setting is missing its file key")
|
||||
})?;
|
||||
let CanonicalValue::ExternalRaw(raw) = value else {
|
||||
return Err(input_error(
|
||||
"Custom settings require a raw string value",
|
||||
));
|
||||
};
|
||||
validate_raw_key_value(&key, raw)?;
|
||||
Ok((key, raw.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_editor(
|
||||
instance_id: &str,
|
||||
state: &State,
|
||||
) -> crate::Result<GameSettingsEditorState> {
|
||||
let (metadata, document, input_bytes, controlled_keys) =
|
||||
load_instance_document(instance_id, state).await?;
|
||||
Ok(build_editor_state(
|
||||
&metadata,
|
||||
&document,
|
||||
&input_bytes,
|
||||
&controlled_keys,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_config(
|
||||
instance_id: &str,
|
||||
) -> crate::Result<GameSettingsEditorState> {
|
||||
let state = State::get().await?;
|
||||
let _guard = state.lock_synced_options().await;
|
||||
load_editor(instance_id, &state).await
|
||||
}
|
||||
|
||||
pub async fn preview_changes(
|
||||
instance_id: &str,
|
||||
request: UpdateGameSettingsRequest,
|
||||
) -> crate::Result<GameSettingsEditorState> {
|
||||
let state = State::get().await?;
|
||||
let _guard = state.lock_synced_options().await;
|
||||
let (metadata, document, input_bytes, controlled_keys) =
|
||||
load_instance_document(instance_id, &state).await?;
|
||||
let mut editor = build_editor_state(
|
||||
&metadata,
|
||||
&document,
|
||||
&input_bytes,
|
||||
&controlled_keys,
|
||||
);
|
||||
let stale = request.expected_catalog_revision != CATALOG_REVISION
|
||||
|| request.expected_summary_revision != editor.summary_revision
|
||||
|| request.expected_canonical_revision != editor.canonical_revision;
|
||||
|
||||
for change in request.changes {
|
||||
let Some(setting) = editor
|
||||
.settings
|
||||
.iter_mut()
|
||||
.find(|setting| setting.option_id == change.option_id)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if stale || change.base_option_revision != setting.option_revision {
|
||||
setting.validation_error =
|
||||
Some(GameOptionValidationIssue::ChangedSinceOpened);
|
||||
continue;
|
||||
}
|
||||
if change.sync_enabled.is_some() {
|
||||
setting.validation_error =
|
||||
Some(GameOptionValidationIssue::InvalidValue);
|
||||
continue;
|
||||
}
|
||||
if let Some(value) = change.canonical_value {
|
||||
match value {
|
||||
Some(value)
|
||||
if encode_setting_value(
|
||||
&metadata, &document, setting, &value,
|
||||
)
|
||||
.is_ok() =>
|
||||
{
|
||||
setting.canonical_value = Some(value);
|
||||
setting.value_state = GameOptionValueState::Canonical;
|
||||
setting.validation_error = None;
|
||||
}
|
||||
_ => {
|
||||
setting.canonical_value = None;
|
||||
setting.value_state = GameOptionValueState::Invalid;
|
||||
setting.validation_error =
|
||||
Some(GameOptionValidationIssue::InvalidValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(editor)
|
||||
}
|
||||
|
||||
pub async fn save_changes(
|
||||
instance_id: &str,
|
||||
request: UpdateGameSettingsRequest,
|
||||
) -> crate::Result<SaveGameSettingsResult> {
|
||||
let state = State::get().await?;
|
||||
let _guard = state.lock_synced_options().await;
|
||||
let (metadata, mut document, input_bytes, controlled_keys) =
|
||||
load_instance_document(instance_id, &state).await?;
|
||||
let current_editor = build_editor_state(
|
||||
&metadata,
|
||||
&document,
|
||||
&input_bytes,
|
||||
&controlled_keys,
|
||||
);
|
||||
let UpdateGameSettingsRequest {
|
||||
expected_summary_revision,
|
||||
expected_canonical_revision,
|
||||
expected_catalog_revision,
|
||||
changes,
|
||||
} = request;
|
||||
if expected_catalog_revision != CATALOG_REVISION
|
||||
|| expected_summary_revision != current_editor.summary_revision
|
||||
|| expected_canonical_revision != current_editor.canonical_revision
|
||||
{
|
||||
return Ok(SaveGameSettingsResult {
|
||||
state: Some(current_editor),
|
||||
conflicts: changes
|
||||
.into_iter()
|
||||
.map(|change| change.option_id)
|
||||
.collect(),
|
||||
..SaveGameSettingsResult::default()
|
||||
});
|
||||
}
|
||||
|
||||
let mut changed = false;
|
||||
for change in changes {
|
||||
let setting = current_editor
|
||||
.settings
|
||||
.iter()
|
||||
.find(|setting| setting.option_id == change.option_id)
|
||||
.ok_or_else(|| {
|
||||
input_error(format!(
|
||||
"Unknown game setting {}",
|
||||
change.option_id
|
||||
))
|
||||
})?;
|
||||
if setting.controlled {
|
||||
return Err(input_error(format!(
|
||||
"{} is controlled by the instance's launcher settings",
|
||||
setting.option_id
|
||||
)));
|
||||
}
|
||||
if change.base_option_revision != setting.option_revision {
|
||||
return Ok(SaveGameSettingsResult {
|
||||
state: Some(current_editor),
|
||||
conflicts: vec![change.option_id],
|
||||
..SaveGameSettingsResult::default()
|
||||
});
|
||||
}
|
||||
if change.sync_enabled.is_some() {
|
||||
return Err(input_error(
|
||||
"Sync selection cannot be changed in the instance editor",
|
||||
));
|
||||
}
|
||||
let Some(Some(value)) = change.canonical_value else {
|
||||
return Err(input_error(format!(
|
||||
"Choose a value for {}",
|
||||
setting.option_id
|
||||
)));
|
||||
};
|
||||
let (key, raw) =
|
||||
encode_setting_value(&metadata, &document, setting, &value)?;
|
||||
changed |= document.set(&key, &raw, false)?;
|
||||
}
|
||||
|
||||
if changed {
|
||||
io::write(options_path(&metadata, &state)?, document.serialize()?)
|
||||
.await?;
|
||||
}
|
||||
let refreshed = load_editor(instance_id, &state).await?;
|
||||
Ok(SaveGameSettingsResult {
|
||||
state: Some(refreshed),
|
||||
applied: u32::from(changed),
|
||||
..SaveGameSettingsResult::default()
|
||||
})
|
||||
}
|
||||
@@ -18,6 +18,7 @@ mod catalog;
|
||||
mod fullscreen;
|
||||
mod instance_support;
|
||||
mod launch_overrides;
|
||||
mod local_settings_editor;
|
||||
pub(crate) mod locales;
|
||||
mod options_file;
|
||||
mod pack_updates;
|
||||
@@ -39,6 +40,10 @@ pub use api_types::{
|
||||
UpdateGameSettingsRequest,
|
||||
};
|
||||
pub use launch_overrides::{apply_launcher_overrides, sync_before_launch};
|
||||
pub use local_settings_editor::{
|
||||
get_config as get_local_config, preview_changes as preview_local_changes,
|
||||
save_changes as save_local_changes,
|
||||
};
|
||||
pub(crate) use locales::{
|
||||
GameLocaleIndexer, queue_game_locale_index, start_game_locale_indexer,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user