CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
357 lines
14 KiB
C#
357 lines
14 KiB
C#
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using Humanizer;
|
|
using PCL.Core.App.Localization;
|
|
using PCL.Core.Logging;
|
|
using PCL.Core.Minecraft.Saves;
|
|
using PCL.Core.Minecraft.Saves.Editing;
|
|
using PCL.Core.UI;
|
|
|
|
namespace PCL;
|
|
|
|
public partial class PageInstanceSavesInfo : IRefreshable
|
|
{
|
|
/// <summary>无状态服务,线程安全,所有实例可共享。</summary>
|
|
private static readonly SaveManager SaveManager = new();
|
|
|
|
/// <summary>防并发冲突</summary>
|
|
private static readonly SemaphoreSlim WriteLock = new(1, 1);
|
|
|
|
private CancellationTokenSource? _cts;
|
|
|
|
public PageInstanceSavesInfo()
|
|
{
|
|
InitializeComponent();
|
|
Loaded += async (_, _) =>
|
|
{
|
|
PanBack.ScrollToHome();
|
|
await RefreshInfoAsync();
|
|
};
|
|
}
|
|
|
|
void IRefreshable.Refresh() => Refresh();
|
|
public void Refresh() => RefreshInfoAsync().ContinueWith(
|
|
t => LogWrapper.Warn(t.Exception, "Saves", "刷新存档信息异常"), //only 兜底
|
|
CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
|
|
|
|
private async Task RefreshInfoAsync()
|
|
{
|
|
_cts?.Cancel();
|
|
_cts?.Dispose();
|
|
_cts = new CancellationTokenSource();
|
|
var ct = _cts.Token;
|
|
|
|
try
|
|
{
|
|
ClearInfoTable();
|
|
PanSettingsList.Children.Clear();
|
|
PanSettingsList.RowDefinitions.Clear();
|
|
Hintversion1_9.Visibility = Visibility.Collapsed;
|
|
Hintversion1_8.Visibility = Visibility.Collapsed;
|
|
Hintversion1_3.Visibility = Visibility.Collapsed;
|
|
PanSettings.Visibility = Visibility.Collapsed;
|
|
|
|
var save = await SaveManager.LoadSaveAsync(PageInstanceSavesLeft.currentSave, ct);
|
|
|
|
ModMain.frmInstanceSavesLeft.ItemDatapack.Visibility =
|
|
save.VersionId is null or < DataVersionBoundaries._17w47a ? Visibility.Collapsed : Visibility.Visible;
|
|
|
|
if (save.VersionName is null)
|
|
{
|
|
if (save.Difficulty.HasValue)
|
|
ShowHint(Hintversion1_9, "Instance.Saves.Info.VersionHint.1_9");
|
|
else if (save.AllowCommands)
|
|
ShowHint(Hintversion1_8, "Instance.Saves.Info.VersionHint.1_8");
|
|
else
|
|
ShowHint(Hintversion1_3, "Instance.Saves.Info.VersionHint.1_3");
|
|
}
|
|
else
|
|
AddInfoRow(Lang.Text("Instance.Saves.Info.Version"), $"{save.VersionName} ({save.VersionId})");
|
|
|
|
AddInfoRow(Lang.Text("Instance.Saves.Info.LevelName"), save.LevelName);
|
|
AddInfoRow(Lang.Text("Instance.Saves.Info.Seed"),
|
|
save.Seed?.ToString() ?? Lang.Text("Instance.Saves.Info.GetFailed"),
|
|
isSeed: true, versionName: save.VersionName);
|
|
AddInfoRow(Lang.Text("Instance.Saves.Info.LastPlayed"),
|
|
Lang.Date(save.LastPlayedUtc.ToLocalTime(), "g"));
|
|
|
|
if (save.Spawn.HasValue)
|
|
{
|
|
var s = save.Spawn.Value;
|
|
AddInfoRow(Lang.Text("Instance.Saves.Info.SpawnPoint"), $"{s.X:F0} / {s.Y:F0} / {s.Z:F0}");
|
|
}
|
|
|
|
AddInfoRow(Lang.Text("Instance.Saves.Info.GameMode"), GameModeName(save.GameMode));
|
|
|
|
AddInfoRow(Lang.Text("Instance.Saves.Info.PlayTime"),
|
|
Lang.TimeSpan(save.PlayTime, 3, false, TimeUnit.Day, TimeUnit.Second));
|
|
|
|
if (save.VersionName is not null || save.Difficulty.HasValue)
|
|
BuildAllowCommandsSetting(save.AllowCommands);
|
|
|
|
if (save.Difficulty.HasValue)
|
|
BuildDifficultySetting(save.IsHardcore, save.IsDifficultyLocked, (int)save.Difficulty.Value);
|
|
|
|
PanContent.Visibility = Visibility.Visible;
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(
|
|
ex,
|
|
Lang.Text("Instance.Saves.Info.Error.LoadFailed"),
|
|
ModBase.LogLevel.Msgbox,
|
|
userSummary: Lang.Text("Instance.Saves.Info.Error.LoadFailed"));
|
|
PanContent.Visibility = Visibility.Collapsed;
|
|
PanSettings.Visibility = Visibility.Collapsed;
|
|
PanSettingsList.Children.Clear();
|
|
PanSettingsList.RowDefinitions.Clear();
|
|
Hintversion1_9.Visibility = Visibility.Collapsed;
|
|
Hintversion1_8.Visibility = Visibility.Collapsed;
|
|
Hintversion1_3.Visibility = Visibility.Collapsed;
|
|
}
|
|
}
|
|
|
|
private void BuildAllowCommandsSetting(bool allowCommands)
|
|
{
|
|
PanSettings.Visibility = Visibility.Visible;
|
|
var folder = PageInstanceSavesLeft.currentSave;
|
|
|
|
var combo = new MyComboBox
|
|
{
|
|
Width = 100d, HorizontalAlignment = HorizontalAlignment.Left,
|
|
ToolTip = Lang.Text("Instance.Saves.Info.Modify.BeforeSave"),
|
|
SelectedValuePath = "Value", DisplayMemberPath = "Display",
|
|
};
|
|
combo.Items.Add(new { Value = 0, Display = Lang.Text("Instance.Saves.Info.AllowCommands.NotAllowed") });
|
|
combo.Items.Add(new { Value = 1, Display = Lang.Text("Instance.Saves.Info.AllowCommands.Allowed") });
|
|
combo.SelectedValue = allowCommands ? 1 : 0;
|
|
|
|
combo.SelectionChanged += async (_, _) =>
|
|
{
|
|
try
|
|
{
|
|
if (combo.SelectedValue is null) return;
|
|
await WriteLock.WaitAsync();
|
|
try
|
|
{
|
|
await SaveManager.ApplyChangesAsync(folder, new SaveChanges
|
|
{
|
|
AllowCommands = new Editable<bool>((int)combo.SelectedValue == 1),
|
|
});
|
|
}
|
|
finally
|
|
{
|
|
WriteLock.Release();
|
|
}
|
|
|
|
HintService.Hint(Lang.Text("Instance.Saves.Info.Modify.CheatSuccess"), HintType.Success);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(
|
|
ex,
|
|
Lang.Text("Instance.Saves.Info.Modify.CheatFailed"),
|
|
ModBase.LogLevel.Hint,
|
|
userSummary: Lang.Text("Instance.Saves.Info.Modify.CheatFailed"));
|
|
}
|
|
};
|
|
|
|
AddSettingRow(Lang.Text("Instance.Saves.Info.AllowCommands"), combo);
|
|
}
|
|
|
|
private void BuildDifficultySetting(bool isHardcore, bool isLocked, int difficultyValue)
|
|
{
|
|
PanSettings.Visibility = Visibility.Visible;
|
|
var folder = PageInstanceSavesLeft.currentSave;
|
|
|
|
var combo = new MyComboBox
|
|
{
|
|
Width = 100d, HorizontalAlignment = HorizontalAlignment.Left,
|
|
ToolTip = Lang.Text("Instance.Saves.Info.Modify.BeforeSave"),
|
|
SelectedValuePath = "Value", DisplayMemberPath = "Display",
|
|
};
|
|
combo.Items.Add(new { Value = 0, Display = Lang.Text("Instance.Saves.Info.Difficulty.Peaceful") });
|
|
combo.Items.Add(new { Value = 1, Display = Lang.Text("Instance.Saves.Info.Difficulty.Easy") });
|
|
combo.Items.Add(new { Value = 2, Display = Lang.Text("Instance.Saves.Info.Difficulty.Normal") });
|
|
combo.Items.Add(new { Value = 3, Display = Lang.Text("Instance.Saves.Info.Difficulty.Hard") });
|
|
combo.SelectedValue = difficultyValue;
|
|
|
|
var lockCheckBox = new MyCheckBox
|
|
{
|
|
Text = Lang.Text("Instance.Saves.Info.LockDifficulty"),
|
|
ToolTip = Lang.Text("Instance.Saves.Info.LockDifficulty.ToolTip"),
|
|
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(10d, 0d, 0d, 0d),
|
|
Checked = isLocked,
|
|
Visibility = isHardcore ? Visibility.Collapsed : Visibility.Visible,
|
|
};
|
|
|
|
var panel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Left };
|
|
panel.Children.Add(combo);
|
|
panel.Children.Add(lockCheckBox);
|
|
|
|
async Task ApplyAsync()
|
|
{
|
|
try
|
|
{
|
|
if (combo.SelectedValue is null) return;
|
|
await WriteLock.WaitAsync();
|
|
try
|
|
{
|
|
await SaveManager.ApplyChangesAsync(folder, new SaveChanges
|
|
{
|
|
Difficulty = new Editable<Difficulty>((Difficulty)(int)combo.SelectedValue),
|
|
LockDifficulty = new Editable<bool>(!isHardcore && lockCheckBox.Checked == true),
|
|
});
|
|
}
|
|
finally
|
|
{
|
|
WriteLock.Release();
|
|
}
|
|
|
|
HintService.Hint(Lang.Text("Instance.Saves.Info.Modify.DifficultySuccess"), HintType.Success);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(
|
|
ex,
|
|
Lang.Text("Instance.Saves.Info.Modify.DifficultyFailed"),
|
|
ModBase.LogLevel.Hint,
|
|
userSummary: Lang.Text("Instance.Saves.Info.Modify.DifficultyFailed"));
|
|
}
|
|
}
|
|
|
|
combo.SelectionChanged += async (_, _) => await ApplyAsync();
|
|
lockCheckBox.Change += async (_, _) => await ApplyAsync();
|
|
|
|
AddSettingRow(Lang.Text("Instance.Saves.Info.GameDifficultyLabel"), panel);
|
|
}
|
|
|
|
private static string GameModeName(GameMode mode) => mode switch
|
|
{
|
|
GameMode.Hardcore => Lang.Text("Instance.Saves.Info.GameMode.Hardcore"),
|
|
GameMode.Creative => Lang.Text("Instance.Saves.Info.GameMode.Creative"),
|
|
GameMode.Adventure => Lang.Text("Instance.Saves.Info.GameMode.Adventure"),
|
|
GameMode.Spectator => Lang.Text("Instance.Saves.Info.GameMode.Spectator"),
|
|
_ => Lang.Text("Instance.Saves.Info.GameMode.Survival"),
|
|
};
|
|
|
|
private static void ShowHint(MyHint hint, string langKey)
|
|
{
|
|
hint.Text = Lang.Text(langKey);
|
|
hint.Visibility = Visibility.Visible;
|
|
}
|
|
|
|
private void ClearInfoTable()
|
|
{
|
|
PanList.Children.Clear();
|
|
PanList.RowDefinitions.Clear();
|
|
}
|
|
|
|
private void AddInfoRow(string head, string content, bool isSeed = false, string? versionName = null)
|
|
{
|
|
var headBlock = new TextBlock { Text = head, Margin = new Thickness(0d, 3d, 0d, 3d) };
|
|
var contentStack = new StackPanel { Orientation = Orientation.Horizontal };
|
|
|
|
if (isSeed && content != Lang.Text("Instance.Saves.Info.GetFailed"))
|
|
{
|
|
var seedBtn = new MyTextButton { Text = content, Margin = new Thickness(0d, 3d, 0d, 3d) };
|
|
seedBtn.Click += (_, _) =>
|
|
{
|
|
try
|
|
{
|
|
ModBase.ClipboardSet(content);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(
|
|
ex,
|
|
Lang.Text("Instance.Saves.Info.Error.ClipboardFailed"),
|
|
ModBase.LogLevel.Hint,
|
|
userSummary: Lang.Text("Instance.Saves.Info.Error.ClipboardFailed"));
|
|
}
|
|
};
|
|
contentStack.Children.Add(seedBtn);
|
|
|
|
var chunkbaseBtn = new MyIconButton
|
|
{
|
|
SvgIcon = "lucide/external-link",
|
|
Width = 22d,
|
|
Height = 22d,
|
|
ToolTip = Lang.Text("Instance.Saves.Info.Chunkbase.ToolTip"),
|
|
};
|
|
chunkbaseBtn.Click += (_, _) => OpenChunkbase(content, versionName);
|
|
contentStack.Children.Add(chunkbaseBtn);
|
|
}
|
|
else
|
|
{
|
|
contentStack.Children.Add(new TextBlock { Text = content, Margin = new Thickness(0d, 3d, 0d, 3d) });
|
|
}
|
|
|
|
PanList.Children.Add(headBlock);
|
|
PanList.Children.Add(contentStack);
|
|
var rowDef = new RowDefinition();
|
|
PanList.RowDefinitions.Add(rowDef);
|
|
var rowIndex = PanList.RowDefinitions.IndexOf(rowDef);
|
|
Grid.SetRow(headBlock, rowIndex);
|
|
Grid.SetColumn(headBlock, 0);
|
|
Grid.SetRow(contentStack, rowIndex);
|
|
Grid.SetColumn(contentStack, 2);
|
|
}
|
|
|
|
private void AddSettingRow(string head, UIElement control)
|
|
{
|
|
var rowIndex = PanSettingsList.RowDefinitions.Count;
|
|
PanSettingsList.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1d, GridUnitType.Auto) });
|
|
|
|
var headBlock = new TextBlock { Text = head, Margin = new Thickness(0d, 3d, 0d, 3d) };
|
|
Grid.SetRow(headBlock, rowIndex);
|
|
Grid.SetColumn(headBlock, 0);
|
|
Grid.SetRow(control, rowIndex);
|
|
Grid.SetColumn(control, 2);
|
|
PanSettingsList.Children.Add(headBlock);
|
|
PanSettingsList.Children.Add(control);
|
|
PanSettingsList.RowDefinitions.Add(new RowDefinition { Height = new GridLength(8d, GridUnitType.Pixel) });
|
|
}
|
|
|
|
private static void OpenChunkbase(string seed, string? versionName)
|
|
{
|
|
try
|
|
{
|
|
if (versionName is null)
|
|
{
|
|
ModBase.Log(
|
|
Lang.Text("Instance.Saves.Info.Chunkbase.UnknownVersion"),
|
|
ModBase.LogLevel.Hint,
|
|
userSummary: Lang.Text("Instance.Saves.Info.Chunkbase.UnknownVersion"));
|
|
return;
|
|
}
|
|
|
|
if (versionName.Any(char.IsLetter))
|
|
{
|
|
ModBase.Log(
|
|
Lang.Text("Instance.Saves.Info.Chunkbase.PreviewVersion", versionName),
|
|
ModBase.LogLevel.Hint,
|
|
userSummary: Lang.Text("Instance.Saves.Info.Chunkbase.PreviewVersion", versionName));
|
|
return;
|
|
}
|
|
|
|
var usedVersion = versionName.StartsWith("1.21")
|
|
? versionName.Replace(".", "_")
|
|
: versionName.Contains('.')
|
|
? string.Join("_", versionName.Split('.').Take(2))
|
|
: versionName.Replace(".", "_");
|
|
ModBase.OpenWebsite(
|
|
$"https://www.chunkbase.com/apps/seed-map#seed={seed}&platform=java_{usedVersion}&dimension=overworld");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(
|
|
ex,
|
|
Lang.Text("Instance.Saves.Info.Error.ChunkbaseFailed"),
|
|
ModBase.LogLevel.Hint,
|
|
userSummary: Lang.Text("Instance.Saves.Info.Error.ChunkbaseFailed"));
|
|
}
|
|
}
|
|
}
|