Files
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

574 lines
20 KiB
C#

using System.Collections.Specialized;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Threading;
using Microsoft.VisualBasic.FileIO;
using PCL.Core.App.Localization;
using PCL.Core.UI;
namespace PCL;
public partial class PageInstanceSaves : IRefreshable
{
private readonly DispatcherTimer fileSystemRefreshTimer;
private readonly DispatcherTimer searchTimer;
private FileSystemWatcher fileSystemWatcher;
private bool isLoad;
private object quickPlayFeature = false;
private List<string> saveFolders = new();
private string worldPath;
public PageInstanceSaves()
{
InitializeComponent();
fileSystemRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100d) };
searchTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100d) };
Loaded += PageSetupLaunch_Loaded;
Unloaded += Page_Unloaded;
fileSystemRefreshTimer.Tick += FileSystemRefreshTimer_Tick;
searchTimer.Tick += SearchTimer_Tick;
SearchBox.TextChanged += SearchRun;
}
void IRefreshable.Refresh()
{
RefreshSelf();
}
private void RefreshSelf()
{
Refresh();
CheckQuickPlay();
}
public static void Refresh()
{
if (ModMain.frmInstanceSaves is not null)
ModMain.frmInstanceSaves.Reload();
ModMain.frmInstanceLeft.ItemWorld.Checked = true;
HintService.Hint(Lang.Text("Instance.Saves.Status.Refreshing"), log: false);
}
private void PageSetupLaunch_Loaded(object sender, RoutedEventArgs e)
{
// 重复加载部分
PanBack.ScrollToHome();
worldPath = PageInstanceLeft.McInstance.PathIndie + @"saves\";
if (!Directory.Exists(worldPath))
Directory.CreateDirectory(worldPath);
Reload();
// 非重复加载部分
if (isLoad)
return;
isLoad = true;
CheckQuickPlay();
// 初始化文件系统监视器和排序按钮
SetupFileSystemWatcher();
BtnSort.Click += BtnSortClick;
SetSortMethod(_currentSortMethod);
}
private string GetFolderNameFromPath(string fullPath)
{
return string.IsNullOrEmpty(fullPath) ? "" :
fullPath.EndsWith(@"\") ? new DirectoryInfo(fullPath).Parent?.Name : new DirectoryInfo(fullPath).Name;
}
private string GetFileNameFromPath(string fullPath)
{
return Path.GetFileName(fullPath);
}
private void SetupFileSystemWatcher()
{
if (fileSystemWatcher is not null) fileSystemWatcher.Dispose();
// 确保目录存在
if (!Directory.Exists(worldPath))
Directory.CreateDirectory(worldPath);
fileSystemWatcher = new FileSystemWatcher();
fileSystemWatcher.Path = worldPath;
fileSystemWatcher.IncludeSubdirectories = false;
fileSystemWatcher.NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.LastWrite;
fileSystemWatcher.Created += OnFileSystemChanged;
fileSystemWatcher.Deleted += OnFileSystemChanged;
fileSystemWatcher.Renamed += OnFileSystemChanged;
fileSystemWatcher.EnableRaisingEvents = true;
}
private void OnFileSystemChanged(object sender, FileSystemEventArgs e)
{
fileSystemRefreshTimer.Stop();
fileSystemRefreshTimer.Start();
}
private void FileSystemRefreshTimer_Tick(object sender, EventArgs e)
{
fileSystemRefreshTimer.Stop();
ModBase.RunInUi(() => Reload(), true);
}
private void Page_Unloaded(object sender, RoutedEventArgs e)
{
if (fileSystemWatcher is not null)
{
fileSystemWatcher.Created -= OnFileSystemChanged;
fileSystemWatcher.Deleted -= OnFileSystemChanged;
fileSystemWatcher.Renamed -= OnFileSystemChanged;
fileSystemWatcher.Dispose();
fileSystemWatcher = null;
}
fileSystemRefreshTimer.Stop();
searchTimer.Stop();
}
/// <summary>
/// 确保当前页面上的信息已正确显示。
/// </summary>
public void Reload()
{
ModAnimation.AniControlEnabled += 1;
PanBack.ScrollToHome();
LoadFileList();
ModAnimation.AniControlEnabled -= 1;
}
private void RefreshUI()
{
try
{
if (IsSearching)
{
var resultCount = _searchResult is null ? 0 : _searchResult.Count;
PanListBack.Title = Lang.Text("Instance.Saves.SearchResultTitle", resultCount.ToString());
}
else
{
PanListBack.Title = Lang.Text("Instance.Saves.SaveListTitle", saveFolders.Count.ToString());
}
if (saveFolders.Count == 0)
{
PanNoWorld.Visibility = Visibility.Visible;
PanContent.Visibility = Visibility.Collapsed;
PanNoWorld.UpdateLayout();
}
else
{
PanNoWorld.Visibility = Visibility.Collapsed;
PanContent.Visibility = Visibility.Visible;
PanContent.UpdateLayout();
var showingSaves = (IsSearching ? _searchResult : saveFolders).ToList();
if (showingSaves.Any())
{
var sortMethod = GetSortMethod(_currentSortMethod);
showingSaves.Sort((a, b) => sortMethod(a, b));
}
ModAnimation.AniControlEnabled += 1;
PanList.Children.Clear();
foreach (var curFolder in showingSaves)
{
// 检查文件夹是否仍然存在
if (!Directory.Exists(curFolder)) continue;
var saveLogo = Path.Combine(curFolder, "icon.png");
var tmpCurFolder = curFolder;
if (File.Exists(saveLogo))
{
var target =
$@"{PageInstanceLeft.McInstance.PathInstance}PCL\ImgCache\{ModBase.GetStringMD5(saveLogo)}.png";
ModBase.CopyFile(saveLogo, target);
saveLogo = target;
}
else
{
saveLogo = ModBase.pathImage + "Icons/NoIcon.png";
}
var worldItem = new MyListItem
{
Logo = saveLogo,
Title = GetFolderNameFromPath(curFolder),
Info =
Lang.Text("Instance.Saves.CreationTime", Lang.Date(Directory.GetCreationTime(curFolder), "d"), Lang.Date(Directory.GetLastWriteTime(curFolder), "d")),
Type = MyListItem.CheckType.Clickable
};
worldItem.Click += (_, _) => ModMain.frmMain.PageChange(new FormMain.PageStackData
{ page = FormMain.PageType.VersionSaves, additional = (null, null, null, ModComp.CompLoaderType.Any, ModComp.CompType.Any, tmpCurFolder) });
var btnOpen = new MyIconButton
{
SvgIcon = "lucide/folder-open",
ToolTip = Lang.Text("Common.Action.Open")
};
btnOpen.Click += (_, _) => ModBase.OpenExplorer(tmpCurFolder);
var btnDelete = new MyIconButton
{
SvgIcon = "lucide/trash-2",
ToolTip = Lang.Text("Common.Action.Delete")
};
btnDelete.Click += (_, _) =>
{
worldItem.IsEnabled = false;
worldItem.Info = Lang.Text("Instance.Saves.Deleting");
ModBase.RunInNewThread(() =>
{
try
{
FileSystem.DeleteDirectory(tmpCurFolder, UIOption.OnlyErrorDialogs,
RecycleOption.SendToRecycleBin);
HintService.Hint(Lang.Text("Instance.Saves.DeletedToRecycleBin"));
ModBase.RunInUiWait(() => RemoveItem(worldItem));
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.DeleteFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.DeleteFailed"));
ModBase.RunInUiWait(() => Reload());
}
});
};
var btnCopy = new MyIconButton
{
SvgIcon = "lucide/copy",
ToolTip = Lang.Text("Common.Action.Copy")
};
btnCopy.Click += (_, _) =>
{
try
{
if (Directory.Exists(tmpCurFolder))
{
Clipboard.SetFileDropList(new StringCollection { tmpCurFolder });
HintService.Hint(Lang.Text("Instance.Saves.CopiedToClipboard"));
HintService.Hint(Lang.Text("Instance.Saves.CopyPasteWarning"));
}
else
{
HintService.Hint(Lang.Text("Instance.Saves.FolderNotFound"));
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.CopyFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.CopyFailed"));
}
};
var btnInfo = new MyIconButton
{
SvgIcon = "lucide/info",
ToolTip = Lang.Text("Instance.Saves.Details")
};
btnInfo.Click += (_, _) => ModMain.frmMain.PageChange(new FormMain.PageStackData
{ page = FormMain.PageType.VersionSaves, additional = (null, null, null, ModComp.CompLoaderType.Any, ModComp.CompType.Any, tmpCurFolder) });
var btnLaunch = new MyIconButton
{
SvgIcon = "lucide/play",
ToolTip = Lang.Text("Instance.Saves.QuickPlay")
};
btnLaunch.Click += (_, _) =>
{
var worldName = GetFileNameFromPath(tmpCurFolder);
var launchOptions = new ModLaunch.McLaunchOptions
{
WorldName = worldName,
instance = PageInstanceLeft.McInstance
};
ModLaunch.McLaunchStart(launchOptions);
ModMain.frmMain.PageChange(new FormMain.PageStackData { page = FormMain.PageType.Launch });
};
if ((bool)quickPlayFeature)
worldItem.Buttons = new[] { btnOpen, btnDelete, btnCopy, btnInfo, btnLaunch };
else
worldItem.Buttons = new[] { btnOpen, btnDelete, btnCopy, btnInfo };
PanList.Children.Add(worldItem);
}
ModAnimation.AniControlEnabled -= 1;
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.RefreshUiFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.RefreshUiFailed"));
}
}
private void CheckQuickPlay()
{
try
{
var cur = new ModLaunch.LaunchArgument(PageInstanceLeft.McInstance);
quickPlayFeature = cur.HasArguments("--quickPlaySingleplayer");
}
catch (Exception ex)
{
ModBase.Log(
ex,
"检查存档快捷启动失败",
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.Error.OperationFailed"));
}
}
private void LoadFileList()
{
try
{
ModBase.Log("[World] 刷新存档文件");
saveFolders.Clear();
if (Directory.Exists(worldPath))
saveFolders = Directory.EnumerateDirectories(worldPath).ToList();
else
saveFolders = new List<string>();
if (ModBase.modeDebug)
ModBase.Log("[World] 共发现 " + saveFolders.Count + " 个存档文件夹", ModBase.LogLevel.Debug);
PanList.Children.Clear();
CheckQuickPlay();
if (ModBase.modeDebug)
{
if ((bool)quickPlayFeature)
ModBase.Log("[World] 该实例支持存档快捷启动", ModBase.LogLevel.Debug);
else
ModBase.Log("[World] 该实例不支持存档快捷启动", ModBase.LogLevel.Debug);
}
RefreshUI(); // 确保UI刷新
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.LoadListFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.LoadListFailed"));
}
}
private void RemoveItem(MyListItem item)
{
if (PanList.Children.IndexOf(item) == -1)
return;
PanList.Children.Remove(item);
RefreshUI();
}
private void BtnOpenFolder_Click(object sender, MouseButtonEventArgs e)
{
ModBase.OpenExplorer(worldPath);
}
private void BtnPaste_Click(object sender, MouseButtonEventArgs e)
{
var files = Clipboard.GetFileDropList();
var loaders = new List<ModLoader.LoaderBase>();
loaders.Add(new ModLoader.LoaderTask<int, int>("Copy saves", _ =>
{
var copied = 0;
foreach (var i in files)
try
{
if (Directory.Exists(i))
{
if (Directory.Exists(worldPath + GetFolderNameFromPath(i)))
{
HintService.Hint(Lang.Text("Instance.Saves.DuplicateFolder", GetFolderNameFromPath(i)));
}
else
{
ModBase.CopyDirectory(i, worldPath + GetFolderNameFromPath(i));
copied += 1;
}
}
else
{
HintService.Hint(Lang.Text("Instance.Saves.SourceNotFolder"));
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Instance.Saves.PasteFolderFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Instance.Saves.PasteFolderFailed"));
}
if (copied > 0)
HintService.Hint(Lang.Text("Instance.Saves.PastedCount", copied.ToString()), HintType.Success);
ModBase.RunInUi(() => Reload());
}));
var loader = new ModLoader.LoaderCombo<int>($"{PageInstanceLeft.McInstance.Name} - {Lang.Text("Instance.Saves.CopySave")}", loaders)
{ OnStateChanged = ModDownloadLib.LoaderStateChangedHintOnly };
loader.Start(1);
ModLoader.LoaderTaskbarAdd(loader);
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
ModMain.frmMain.BtnExtraDownload.Ribble();
}
private void BtnDownloadNew_Click(object sender, MouseButtonEventArgs e)
{
ModMain.frmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadWorld);
PageComp.targetVersion = PageInstanceLeft.McInstance; // 将当前实例设置为筛选器
}
#region 搜索和排序
private SortMethod _currentSortMethod = SortMethod.FileName;
private List<string> _searchResult;
public bool IsSearching => !string.IsNullOrWhiteSpace(SearchBox.Text);
private enum SortMethod
{
FileName,
CreateTime,
ModifyTime
}
private string GetSortName(SortMethod method)
{
switch (method)
{
case SortMethod.FileName:
{
return Lang.Text("Instance.Saves.SortFileName");
}
case SortMethod.CreateTime:
{
return Lang.Text("Instance.Saves.SortCreateTime");
}
case SortMethod.ModifyTime:
{
return Lang.Text("Instance.Saves.SortModifyTime");
}
default:
{
return Lang.Text("Instance.Saves.SortFileName");
}
}
}
private void SetSortMethod(SortMethod target)
{
_currentSortMethod = target;
BtnSort.Text = Lang.Text("Instance.Saves.SortBy", GetSortName(target));
RefreshUI();
}
private void BtnSortClick(object sender, EventArgs e)
{
var body = new ContextMenu();
foreach (SortMethod i in Enum.GetValues(typeof(SortMethod)))
{
var item = new MyMenuItem();
item.Header = GetSortName(i);
item.Click += (_, _) => SetSortMethod(i);
body.Items.Add(item);
}
body.PlacementTarget = (UIElement)sender;
body.Placement = PlacementMode.Bottom;
body.IsOpen = true;
}
private void SearchRun(object sender, EventArgs e)
{
searchTimer.Stop();
searchTimer.Start();
}
private void SearchTimer_Tick(object sender, EventArgs e)
{
searchTimer.Stop();
PerformSearch();
}
private void PerformSearch()
{
try
{
if (IsSearching)
{
var queryList = new List<ModBase.SearchEntry<string>>();
foreach (var saveFolder in saveFolders)
{
var folderName = GetFolderNameFromPath(saveFolder);
var searchSource = new List<ModBase.SearchSource>();
searchSource.Add(new ModBase.SearchSource(folderName, 1d));
queryList.Add(new ModBase.SearchEntry<string> { item = saveFolder, searchSource = searchSource });
}
_searchResult = ModBase.Search(queryList, SearchBox.Text, ModBase.MaxLocalSearchDepth, 0.35d).Select(r => r.item).ToList();
}
else
{
_searchResult = null;
}
RefreshUI();
}
catch (Exception ex)
{
ModBase.Log(ex, Lang.Text("Instance.Saves.SearchError"));
}
}
private Func<string, string, int> GetSortMethod(SortMethod method)
{
switch (method)
{
case SortMethod.FileName:
{
return (a, b) => string.Compare(GetFolderNameFromPath(a), GetFolderNameFromPath(b),
StringComparison.OrdinalIgnoreCase);
}
case SortMethod.CreateTime:
{
return (a, b) => Directory.GetCreationTime(b).CompareTo(Directory.GetCreationTime(a));
}
case SortMethod.ModifyTime:
{
return (a, b) => Directory.GetLastWriteTime(b).CompareTo(Directory.GetLastWriteTime(a));
}
default:
{
return (a, b) => string.Compare(GetFolderNameFromPath(a), GetFolderNameFromPath(b),
StringComparison.OrdinalIgnoreCase);
}
}
}
#endregion
}