Files
Starlight_Lancher/packages/app-lib/src/api/instance/synced_servers/operations.rs
T
AxTps ed61a5942c
Sync source to CNB / Sync Git ref (push) Has been cancelled
feat: 跨实例同步移植 WIP 检查点(第1轮+2a+2b+synced_servers)
从 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轮),当前编译不通。
2026-10-03 22:18:37 +08:00

1038 lines
34 KiB
Rust

use super::super::synced_options::{
CheckpointStatus, checkpoint, detach_link, ensure_link, finish_checkpoint,
instance_game_dir, instance_is_running, instance_option_enabled,
instance_option_supported, sha1_bytes, sha1_file, sync_files_are_protected,
};
use crate::state::{InstanceMetadata, SyncedOption};
use crate::{ErrorKind, State};
use quartz_nbt::NbtCompound;
use uuid::Uuid;
use super::SERVERS_FILE;
use super::codec::{
read_servers, server_address, server_hidden, server_identity_address,
server_identity_name, servers_to_bytes, update_server_data, write_servers,
};
use super::modpack::{
is_modpack_link, pack_state_exists, pack_state_matches_link,
reconstruct_modpack_servers,
};
use super::storage::{
begin_server_checkpoint, canonical_exists, commit_server_state,
generated_path, load_local, load_projection_entries, read_canonical,
read_server_snapshot, server_revision, write_local,
};
use super::types::{
CanonicalServer, DesyncServerMode, LocalServer, ProjectionEntry,
ProjectionOwner, ServerRecord, ServerSource, SyncedServer,
};
use std::collections::HashSet;
fn normalized_server_address(data: &NbtCompound) -> (String, u16) {
let address = server_identity_address(data);
let (host, port) = crate::server_address::parse_server_address(&address)
.unwrap_or((&address, 25565));
(host.trim().to_string(), port)
}
async fn sync_candidates(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<Vec<NbtCompound>> {
let locals = load_local(&metadata.instance.id, state).await?;
let mut servers =
read_servers(&instance_game_dir(metadata, state)?.join(SERVERS_FILE))
.await?;
for local in locals {
let local_address = normalized_server_address(&local.data);
let index = servers
.iter()
.position(|data| data == &local.data)
.or_else(|| {
servers.iter().position(|data| {
!local_address.0.is_empty()
&& normalized_server_address(data) == local_address
})
});
if let Some(index) = index {
servers.remove(index);
}
}
Ok(servers)
}
pub(in crate::api::instance) async fn seed_servers(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<()> {
if is_modpack_link(&metadata.link)
&& (!pack_state_matches_link(metadata, state).await?
|| !pack_state_exists(&metadata.instance.id, state).await?)
&& let Err(error) = reconstruct_modpack_servers(metadata, state).await
{
tracing::warn!(
"Failed to reconstruct the server pack state for {}: {error}",
metadata.instance.id
);
}
if is_modpack_link(&metadata.link)
&& !pack_state_exists(&metadata.instance.id, state).await?
{
return Err(ErrorKind::InputError(
"This linked modpack cannot be used as the multiplayer base until its supplied server list has been reconstructed. Choose an unmanaged instance instead."
.to_string(),
)
.into());
}
let servers = sync_candidates(metadata, state).await?;
let canonical = servers
.into_iter()
.map(|data| CanonicalServer {
id: Uuid::new_v4().to_string(),
data,
})
.collect::<Vec<_>>();
commit_server_state(Some(&canonical), None, state).await?;
regenerate_servers(state).await
}
pub(in crate::api::instance) async fn merge_servers_from_instance(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<()> {
if is_modpack_link(&metadata.link)
&& (!pack_state_matches_link(metadata, state).await?
|| !pack_state_exists(&metadata.instance.id, state).await?)
&& let Err(error) = reconstruct_modpack_servers(metadata, state).await
{
tracing::warn!(
"Failed to reconstruct the server pack state for {}: {error}",
metadata.instance.id
);
}
if is_modpack_link(&metadata.link)
&& !pack_state_exists(&metadata.instance.id, state).await?
{
return Err(ErrorKind::InputError(
"This linked modpack cannot join multiplayer syncing until its supplied server list has been reconstructed."
.to_string(),
)
.into());
}
let candidates = sync_candidates(metadata, state).await?;
let mut canonical = read_canonical(state).await?;
for data in candidates {
let address = server_identity_address(&data);
let exists = canonical.iter().any(|server| {
server.data == data
|| (!address.is_empty()
&& server_identity_address(&server.data) == address)
});
if !exists {
canonical.push(CanonicalServer {
id: Uuid::new_v4().to_string(),
data,
});
}
}
if commit_server_state(Some(&canonical), None, state).await? {
regenerate_servers(state).await?;
}
Ok(())
}
pub(in crate::api::instance) async fn ensure_servers(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<()> {
if is_modpack_link(&metadata.link)
&& !pack_state_matches_link(metadata, state).await?
&& let Err(error) = reconstruct_modpack_servers(metadata, state).await
{
tracing::warn!(
"Failed to reconstruct the server pack state for {}; retaining its last known per-instance server metadata: {error}",
metadata.instance.id
);
}
if !canonical_exists(state).await? {
if !is_modpack_link(&metadata.link) {
let servers = sync_candidates(metadata, state).await?;
let canonical = servers
.into_iter()
.map(|data| CanonicalServer {
id: Uuid::new_v4().to_string(),
data,
})
.collect::<Vec<_>>();
commit_server_state(Some(&canonical), None, state).await?;
} else {
commit_server_state(Some(&[]), None, state).await?;
}
}
compose_instance(metadata, state).await
}
pub(in crate::api::instance) async fn detach_servers(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<()> {
let generated = generated_path(state, &metadata.instance.id);
let local = instance_game_dir(metadata, state)?.join(SERVERS_FILE);
detach_link(&generated, &local).await
}
pub(in crate::api::instance) async fn reconcile_servers(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<()> {
if !effective(metadata, state).await? {
return Ok(());
}
let local_path = instance_game_dir(metadata, state)?.join(SERVERS_FILE);
if !local_path.exists() {
return compose_instance(metadata, state).await;
}
let checkpoint = checkpoint(
&metadata.instance.id,
SyncedOption::MultiplayerServers,
"default",
state,
)
.await?;
if checkpoint
.as_ref()
.is_some_and(|value| value.status == CheckpointStatus::Pending)
{
return compose_instance(metadata, state).await;
}
let actual = sha1_file(&local_path).await?;
let revision = server_revision(state).await?;
if checkpoint
.as_ref()
.map(|value| value.expected_sha1.as_str())
== Some(actual.as_str())
{
if checkpoint
.as_ref()
.is_some_and(|value| value.source_revision == revision)
{
return Ok(());
}
return compose_instance(metadata, state).await;
}
let current = read_servers(&local_path).await?;
let projections =
load_projection_entries(&metadata.instance.id, state).await?;
let projection_matches = match_projection_entries(&current, &projections);
let mut matched = HashSet::new();
let mut canonical = read_canonical(state).await?;
let mut locals = load_local(&metadata.instance.id, state).await?;
let mut canonical_order = Vec::new();
for ((position, data), projection) in
current.into_iter().enumerate().zip(projection_matches)
{
if let Some(projection) = projection {
matched.insert(projection.id.clone());
match projection.owner {
ProjectionOwner::Synced => {
canonical_order.push(projection.id.clone());
if let Some(server) = canonical
.iter_mut()
.find(|server| server.id == projection.id)
&& data != projection.data
{
server.data = data;
}
}
ProjectionOwner::Instance => {
if let Some(server) = locals
.iter_mut()
.find(|server| server.id == projection.id)
{
if data != projection.data {
server.data = data;
}
server.position = position as i64;
}
}
}
} else {
let id = Uuid::new_v4().to_string();
canonical_order.push(id.clone());
canonical.push(CanonicalServer { id, data });
}
}
canonical.retain(|server| {
!projections.iter().any(|projection| {
projection.owner == ProjectionOwner::Synced
&& projection.id == server.id
&& !matched.contains(&projection.id)
})
});
locals.retain(|server| {
!projections.iter().any(|projection| {
projection.owner == ProjectionOwner::Instance
&& projection.id == server.id
&& !matched.contains(&projection.id)
})
});
let by_id = canonical
.iter()
.cloned()
.map(|server| (server.id.clone(), server))
.collect::<std::collections::HashMap<_, _>>();
let visible = canonical_order.iter().cloned().collect::<HashSet<_>>();
let mut ordered = canonical_order
.into_iter()
.filter_map(|id| by_id.get(&id).cloned());
for server in &mut canonical {
if visible.contains(&server.id)
&& let Some(next) = ordered.next()
{
*server = next;
}
}
let canonical_changed = commit_server_state(
Some(&canonical),
Some((&metadata.instance.id, &locals)),
state,
)
.await?;
if canonical_changed {
regenerate_servers(state).await
} else {
compose_instance(metadata, state).await
}
}
fn match_projection_entries<'a>(
current: &[NbtCompound],
projections: &'a [ProjectionEntry],
) -> Vec<Option<&'a ProjectionEntry>> {
let mut matches = vec![None; current.len()];
let mut matched_projections = HashSet::new();
for (position, data) in current.iter().enumerate() {
let candidate = projections
.iter()
.enumerate()
.filter(|(index, projection)| {
!matched_projections.contains(index) && projection.data == *data
})
.min_by_key(|(_, projection)| {
projection.position.abs_diff(position as i64)
})
.map(|(index, _)| index);
if let Some(index) = candidate {
matches[position] = Some(index);
matched_projections.insert(index);
}
}
for (position, data) in current.iter().enumerate() {
if matches[position].is_some() {
continue;
}
let address = server_identity_address(data);
if address.is_empty() {
continue;
}
let candidate = projections
.iter()
.enumerate()
.filter(|(index, projection)| {
!matched_projections.contains(index)
&& server_identity_address(&projection.data) == address
})
.min_by_key(|(_, projection)| {
projection.position.abs_diff(position as i64)
})
.map(|(index, _)| index);
if let Some(index) = candidate {
matches[position] = Some(index);
matched_projections.insert(index);
}
}
for (position, data) in current.iter().enumerate() {
if matches[position].is_some() {
continue;
}
let name = server_identity_name(data);
if name.is_empty() {
continue;
}
let candidate = projections
.iter()
.enumerate()
.filter(|(index, projection)| {
!matched_projections.contains(index)
&& server_identity_name(&projection.data) == name
})
.min_by_key(|(_, projection)| {
projection.position.abs_diff(position as i64)
})
.map(|(index, _)| index);
if let Some(index) = candidate {
matches[position] = Some(index);
matched_projections.insert(index);
}
}
matches
.into_iter()
.map(|projection| projection.map(|index| &projections[index]))
.collect()
}
#[tracing::instrument(name = "server_records", skip_all, fields(instance_id = %metadata.instance.id))]
pub(crate) async fn list_server_records(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<Vec<ServerRecord>> {
if participating(metadata, state).await? {
let (canonical, locals) =
read_server_snapshot(&metadata.instance.id, state).await?;
return Ok(merge_server_records(canonical, locals));
}
list_local_server_records(metadata, state).await
}
async fn list_server_records_locked(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<Vec<ServerRecord>> {
if participating(metadata, state).await? {
return compose_records(metadata, state).await;
}
list_local_server_records(metadata, state).await
}
async fn list_local_server_records(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<Vec<ServerRecord>> {
Ok(
read_servers(&instance_game_dir(metadata, state)?.join(SERVERS_FILE))
.await?
.into_iter()
.map(|data| ServerRecord {
id: Uuid::new_v4().to_string(),
source: ServerSource::LocalDesynced,
data,
})
.collect(),
)
}
/// Keeps an automatically added server local, even before server sync is enabled.
pub(crate) async fn ensure_managed_server(
metadata: &InstanceMetadata,
data: NbtCompound,
state: &State,
) -> crate::Result<()> {
let _guard = state.lock_synced_options().await;
let path = instance_game_dir(metadata, state)?.join(SERVERS_FILE);
let syncing = participating(metadata, state).await?;
let records = list_server_records_locked(metadata, state).await?;
let address = normalized_server_address(&data);
let name = server_identity_name(&data);
let existing_index = records
.iter()
.position(|record| {
normalized_server_address(&record.data) == address
&& server_identity_name(&record.data) == name
})
.or_else(|| {
records.iter().position(|record| {
normalized_server_address(&record.data) == address
})
});
let existing = existing_index.map(|index| &records[index]);
let position = existing_index.unwrap_or_else(|| {
records
.iter()
.position(ServerRecord::hidden)
.unwrap_or(records.len())
});
let data = existing
.map(|record| {
let mut existing_data = record.data.clone();
if record.hidden() {
existing_data.insert(
"name",
data.get::<_, &str>("name").unwrap_or_default().to_string(),
);
existing_data.insert("hidden", 0_i8);
}
existing_data
})
.unwrap_or(data);
let excluded_synced_server_id = existing
.filter(|record| record.source == ServerSource::UserSynced)
.map(|record| record.id.clone());
let mut locals = load_local(&metadata.instance.id, state).await?;
if let Some(local) = locals
.iter_mut()
.find(|server| normalized_server_address(&server.data) == address)
{
let excluded_synced_server_id = excluded_synced_server_id
.or_else(|| local.excluded_synced_server_id.clone());
if existing.is_some_and(|record| record.data == data)
&& local.data == data
&& local.position == position as i64
&& local.excluded_synced_server_id == excluded_synced_server_id
&& (!syncing || path.exists())
{
return Ok(());
}
local.data = data.clone();
local.position = position as i64;
local.excluded_synced_server_id = excluded_synced_server_id;
} else {
locals.push(LocalServer {
id: Uuid::new_v4().to_string(),
source: ServerSource::LinkedServerProject,
excluded_synced_server_id,
data: data.clone(),
position: position as i64,
});
}
write_local(&metadata.instance.id, &locals, state).await?;
if syncing {
if effective(metadata, state).await? {
compose_instance(metadata, state).await?;
}
} else if existing.is_none_or(|record| record.data != data) {
let mut servers = records
.into_iter()
.map(|record| record.data)
.collect::<Vec<_>>();
if let Some(index) = existing_index {
servers[index] = data;
} else {
servers.insert(position, data);
}
write_servers(&path, &servers).await?;
}
Ok(())
}
pub(crate) async fn add_user_server(
metadata: &InstanceMetadata,
mut data: NbtCompound,
state: &State,
) -> crate::Result<String> {
let _guard = state.lock_synced_options().await;
let id = Uuid::new_v4().to_string();
if participating(metadata, state).await? {
let mut canonical = read_canonical(state).await?;
canonical.push(CanonicalServer {
id: id.clone(),
data,
});
commit_server_state(Some(&canonical), None, state).await?;
regenerate_servers(state).await?;
} else {
let path = instance_game_dir(metadata, state)?.join(SERVERS_FILE);
let mut servers = read_servers(&path).await?;
let insert_index = servers
.iter()
.position(server_hidden)
.unwrap_or(servers.len());
servers.insert(insert_index, std::mem::take(&mut data));
write_servers(&path, &servers).await?;
}
Ok(id)
}
async fn update_server_locked(
metadata: &InstanceMetadata,
server_id: &str,
data: NbtCompound,
state: &State,
) -> crate::Result<()> {
if participating(metadata, state).await? {
let records = compose_records(metadata, state).await?;
let record = records
.iter()
.find(|record| record.id == server_id)
.ok_or_else(|| {
ErrorKind::InputError("Unknown server".to_string())
})?;
if record.source == ServerSource::UserSynced {
let mut canonical = read_canonical(state).await?;
let server = canonical
.iter_mut()
.find(|server| server.id == server_id)
.ok_or_else(|| {
ErrorKind::InputError("Unknown server".to_string())
})?;
server.data = data;
commit_server_state(Some(&canonical), None, state).await?;
regenerate_servers(state).await?;
} else {
let mut locals = load_local(&metadata.instance.id, state).await?;
let server = locals
.iter_mut()
.find(|server| server.id == server_id)
.ok_or_else(|| {
ErrorKind::InputError("Unknown server".to_string())
})?;
server.data = data;
write_local(&metadata.instance.id, &locals, state).await?;
compose_instance(metadata, state).await?;
}
return Ok(());
}
Err(ErrorKind::InputError(
"Stable server editing requires multiplayer syncing for this instance."
.to_string(),
)
.into())
}
pub(crate) async fn update_server_by_index(
metadata: &InstanceMetadata,
index: usize,
name: String,
address: String,
accept_textures: Option<bool>,
state: &State,
) -> crate::Result<()> {
let _guard = state.lock_synced_options().await;
if participating(metadata, state).await? {
let records = list_server_records_locked(metadata, state).await?;
let record = records
.get(index)
.filter(|record| !record.hidden())
.ok_or_else(|| {
ErrorKind::InputError(format!(
"No editable server at index {index}"
))
})?;
let mut data = record.data.clone();
update_server_data(&mut data, name, address, accept_textures);
return update_server_locked(metadata, &record.id, data, state).await;
}
let path = instance_game_dir(metadata, state)?.join(SERVERS_FILE);
let mut servers = read_servers(&path).await?;
let server = servers
.get_mut(index)
.filter(|server| !server_hidden(server))
.ok_or_else(|| {
ErrorKind::InputError(format!(
"No editable server at index {index}"
))
})?;
update_server_data(server, name, address, accept_textures);
write_servers(&path, &servers).await
}
async fn remove_server_locked(
metadata: &InstanceMetadata,
server_id: &str,
state: &State,
) -> crate::Result<()> {
if !participating(metadata, state).await? {
return Err(ErrorKind::InputError(
"Stable server removal requires multiplayer syncing for this instance."
.to_string(),
)
.into());
}
let records = compose_records(metadata, state).await?;
let record = records
.iter()
.find(|record| record.id == server_id)
.ok_or_else(|| ErrorKind::InputError("Unknown server".to_string()))?;
if record.source == ServerSource::UserSynced {
let mut canonical = read_canonical(state).await?;
canonical.retain(|server| server.id != server_id);
commit_server_state(Some(&canonical), None, state).await?;
regenerate_servers(state).await
} else {
let mut locals = load_local(&metadata.instance.id, state).await?;
locals.retain(|server| server.id != server_id);
write_local(&metadata.instance.id, &locals, state).await?;
compose_instance(metadata, state).await
}
}
pub(crate) async fn remove_server_by_index(
metadata: &InstanceMetadata,
index: usize,
state: &State,
) -> crate::Result<()> {
let _guard = state.lock_synced_options().await;
if participating(metadata, state).await? {
let records = list_server_records_locked(metadata, state).await?;
let record = records
.get(index)
.filter(|record| !record.hidden())
.ok_or_else(|| {
ErrorKind::InputError(format!(
"No removable server at index {index}"
))
})?;
return remove_server_locked(metadata, &record.id, state).await;
}
let path = instance_game_dir(metadata, state)?.join(SERVERS_FILE);
let mut servers = read_servers(&path).await?;
if servers.get(index).is_none_or(server_hidden) {
return Err(ErrorKind::InputError(format!(
"No removable server at index {index}"
))
.into());
}
servers.remove(index);
write_servers(&path, &servers).await
}
pub async fn desync_server(
instance_id: &str,
server_id: &str,
mode: DesyncServerMode,
) -> crate::Result<()> {
let state = State::get().await?;
let _guard = state.lock_synced_options().await;
let metadata = crate::state::get_instance(instance_id, &state.pool)
.await?
.ok_or_else(|| ErrorKind::InputError("Unknown instance".to_string()))?;
if !participating(&metadata, &state).await? {
return Err(ErrorKind::InputError(
"This instance is not participating in multiplayer server syncing."
.to_string(),
)
.into());
}
let mut canonical = read_canonical(&state).await?;
let server = canonical
.iter()
.find(|server| server.id == server_id)
.cloned()
.ok_or_else(|| {
ErrorKind::InputError(
"Only a synced user server can be desynced.".to_string(),
)
})?;
let local_position = compose_records(&metadata, &state)
.await?
.iter()
.position(|record| record.id == server_id)
.unwrap_or_default() as i64;
let mut locals = load_local(instance_id, &state).await?;
locals.push(LocalServer {
id: Uuid::new_v4().to_string(),
source: ServerSource::LocalDesynced,
excluded_synced_server_id: (mode
== DesyncServerMode::KeepInOtherInstances)
.then(|| server.id.clone()),
data: server.data,
position: local_position,
});
if mode == DesyncServerMode::RemoveFromOtherInstances {
canonical.retain(|candidate| candidate.id != server_id);
}
let canonical_changed = commit_server_state(
Some(&canonical),
Some((instance_id, &locals)),
&state,
)
.await?;
if canonical_changed {
regenerate_servers(&state).await?;
} else {
compose_instance(&metadata, &state).await?;
}
Ok(())
}
pub async fn list_synced_servers() -> crate::Result<Vec<SyncedServer>> {
let state = State::get().await?;
let _guard = state.lock_synced_options().await;
Ok(read_canonical(&state)
.await?
.into_iter()
.map(|server| {
let CanonicalServer { id, data } = server;
SyncedServer {
id,
name: data
.get::<_, &str>("name")
.unwrap_or_default()
.to_string(),
address: server_address(&data),
accept_textures: data
.get::<_, i8>("acceptTextures")
.ok()
.map(|value| value != 0),
}
})
.collect())
}
pub async fn update_synced_server(server: SyncedServer) -> crate::Result<()> {
let state = State::get().await?;
let _guard = state.lock_synced_options().await;
let mut canonical = read_canonical(&state).await?;
let target = canonical
.iter_mut()
.find(|candidate| candidate.id == server.id)
.ok_or_else(|| {
ErrorKind::InputError("Unknown synced server".to_string())
})?;
update_server_data(
&mut target.data,
server.name,
server.address,
server.accept_textures,
);
commit_server_state(Some(&canonical), None, &state).await?;
regenerate_servers(&state).await
}
pub async fn remove_synced_server(server_id: &str) -> crate::Result<()> {
let state = State::get().await?;
let _guard = state.lock_synced_options().await;
let mut canonical = read_canonical(&state).await?;
let previous_len = canonical.len();
canonical.retain(|server| server.id != server_id);
if canonical.len() == previous_len {
return Err(
ErrorKind::InputError("Unknown synced server".to_string()).into()
);
}
commit_server_state(Some(&canonical), None, &state).await?;
regenerate_servers(&state).await
}
pub(super) async fn compose_instance(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<()> {
let records = compose_records(metadata, state).await?;
let bytes = servers_to_bytes(
&records
.iter()
.map(|record| record.data.clone())
.collect::<Vec<_>>(),
)?;
let expected = sha1_bytes(&bytes);
let revision = server_revision(state).await?;
begin_server_checkpoint(
&metadata.instance.id,
&records,
&expected,
revision,
state,
)
.await?;
let generated = generated_path(state, &metadata.instance.id);
if let Some(parent) = generated.parent() {
crate::util::io::create_dir_all(parent).await?;
}
crate::util::io::write(&generated, &bytes).await?;
let target = instance_game_dir(metadata, state)?.join(SERVERS_FILE);
let mode = ensure_link(&generated, &target).await?;
finish_checkpoint(
&metadata.instance.id,
SyncedOption::MultiplayerServers,
"default",
mode,
state,
)
.await
}
async fn compose_records(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<Vec<ServerRecord>> {
let canonical = read_canonical(state).await?;
let locals = load_local(&metadata.instance.id, state).await?;
Ok(merge_server_records(canonical, locals))
}
fn merge_server_records(
canonical: Vec<CanonicalServer>,
locals: Vec<LocalServer>,
) -> Vec<ServerRecord> {
let exclusions = locals
.iter()
.filter_map(|server| server.excluded_synced_server_id.as_deref())
.collect::<HashSet<_>>();
let mut records = canonical
.into_iter()
.filter(|server| !exclusions.contains(server.id.as_str()))
.map(|server| ServerRecord {
id: server.id,
source: ServerSource::UserSynced,
data: server.data,
})
.collect::<Vec<_>>();
let mut locals = locals;
locals.sort_by_key(|server| server.position);
for server in locals {
let position = usize::try_from(server.position)
.unwrap_or(records.len())
.min(records.len());
records.insert(
position,
ServerRecord {
id: server.id,
source: server.source,
data: server.data,
},
);
}
records
}
async fn regenerate_servers(state: &State) -> crate::Result<()> {
for metadata in crate::state::list_instances(&state.pool).await? {
if effective(&metadata, state).await? {
compose_instance(&metadata, state).await?;
}
}
Ok(())
}
pub(super) async fn effective(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<bool> {
if sync_files_are_protected(metadata)
|| instance_is_running(metadata, state).await?
{
return Ok(false);
}
participating(metadata, state).await
}
async fn participating(
metadata: &InstanceMetadata,
state: &State,
) -> crate::Result<bool> {
if !instance_option_enabled(metadata, SyncedOption::MultiplayerServers) {
return Ok(false);
}
let global_enabled: bool = sqlx::query_scalar(
"SELECT EXISTS(
SELECT 1 FROM sync_feature_settings
WHERE feature = 'multiplayer_servers' AND globally_enabled = 1
)",
)
.fetch_one(&state.pool)
.await?;
Ok(instance_option_supported(
metadata,
SyncedOption::MultiplayerServers,
global_enabled,
state,
)
.await)
}
#[cfg(test)]
mod tests {
use super::*;
fn server(name: &str, address: &str) -> NbtCompound {
super::super::codec::server_data(
name.to_string(),
address.to_string(),
None,
)
}
#[test]
fn merge_preserves_local_entries_and_synced_exclusions() {
let records = merge_server_records(
vec![
CanonicalServer {
id: "synced-a".to_string(),
data: server("Synced A", "a.example.test"),
},
CanonicalServer {
id: "synced-b".to_string(),
data: server("Synced B", "b.example.test"),
},
],
vec![
LocalServer {
id: "local".to_string(),
source: ServerSource::Modpack,
excluded_synced_server_id: None,
data: server("Pack server", "pack.example.test"),
position: 1,
},
LocalServer {
id: "desynced-b".to_string(),
source: ServerSource::LocalDesynced,
excluded_synced_server_id: Some("synced-b".to_string()),
data: server("Private B", "b.example.test"),
position: 2,
},
],
);
assert_eq!(
records
.iter()
.map(|record| (record.id.as_str(), record.source))
.collect::<Vec<_>>(),
vec![
("synced-a", ServerSource::UserSynced),
("local", ServerSource::Modpack),
("desynced-b", ServerSource::LocalDesynced),
]
);
}
#[test]
fn projection_matching_prefers_exact_then_address_then_name() {
let projections = vec![
ProjectionEntry {
id: "exact".to_string(),
owner: ProjectionOwner::Synced,
data: server("Exact", "exact.example.test"),
position: 2,
},
ProjectionEntry {
id: "address".to_string(),
owner: ProjectionOwner::Synced,
data: server("Old name", "same.example.test"),
position: 0,
},
ProjectionEntry {
id: "name".to_string(),
owner: ProjectionOwner::Instance,
data: server("Same name", "old.example.test"),
position: 1,
},
];
let current = vec![
server("Exact", "exact.example.test"),
server("New name", "same.example.test"),
server("Same name", "new.example.test"),
];
let matches = match_projection_entries(&current, &projections);
assert_eq!(
matches
.into_iter()
.map(|projection| projection
.map(|projection| projection.id.as_str()))
.collect::<Vec<_>>(),
vec![Some("exact"), Some("address"), Some("name")]
);
}
}