Skip to content

feat(skins): 添加离线自定义皮肤支持 - #3518

Open
sysfox wants to merge 24 commits into
PCL-Community:devfrom
sysfox:skins
Open

feat(skins): 添加离线自定义皮肤支持#3518
sysfox wants to merge 24 commits into
PCL-Community:devfrom
sysfox:skins

Conversation

@sysfox

@sysfox sysfox commented Aug 9, 2026

Copy link
Copy Markdown

实现离线自定义皮肤支持

本地皮肤验证

image image image image image

Littleskin验证

image image

Summary by Sourcery

为旧版(离线)账户添加离线自定义皮肤支持,包括本地文件和 Custom Skin Loader API,并将其集成到启动流程和档案(Profile)管理中。

新特性:

  • 引入一个可配置的离线皮肤模型,支持默认皮肤、内置 Steve/Alex、本地文件、LittleSkin,以及 Custom Skin Loader API 等来源。
  • 提供一个通过 authlib-injector 注入的、兼容 Yggdrasil 的嵌入式 HTTP 皮肤服务器,使离线账户也能在游戏中使用自定义皮肤和披风。
  • 添加一个用于配置离线账户皮肤和披风的 WPF 对话框,包括模型选择和 Custom Skin Loader API URL。
  • 按档案(Profile)持久化离线皮肤配置,并在头像展示和游戏启动时加载该配置。

改进:

  • 扩展 HTTP 服务器路由,支持带参数的路径模板,并使启动/释放操作具备幂等性且更适配 IPv6。
  • 改进旧版皮肤加载机制,优先使用已配置的离线自定义皮肤,并导出临时纹理用于头像渲染。

测试:

  • 添加关于皮肤纹理哈希、规范化、序列化的单元测试,以及离线皮肤服务器的元数据与档案(Profile)端点的测试。
Original summary in English

Summary by Sourcery

Add offline custom skin support for legacy (offline) accounts, including local files and Custom Skin Loader APIs, and integrate it into launch flow and profile management.

New Features:

  • Introduce a configurable offline skin model supporting default, built-in Steve/Alex, local files, LittleSkin, and Custom Skin Loader API sources.
  • Provide an embedded Yggdrasil-compatible HTTP skin server injected via authlib-injector so offline accounts can use custom skins and capes in-game.
  • Add a WPF dialog for configuring offline account skins and capes, including model selection and Custom Skin Loader API URL.
  • Persist offline skin configuration per profile and load it for both avatar display and game launches.

Enhancements:

  • Extend HTTP server routing with parameterized path templates and make start/dispose operations idempotent and IPv6-friendly.
  • Improve legacy skin loading to prefer configured offline custom skins and export temporary textures for avatar rendering.

Tests:

  • Add unit tests for skin texture hashing, normalization, serialization, and for the offline skin server metadata and profile endpoints.

sysfox added 13 commits August 9, 2026 13:21
- SkinType/TextureModel/Skin 皮肤配置模型,含 JSON 序列化与容错反序列化
- SkinTexture:贴图内容哈希(SHA256)与进程内缓存,对齐 HMCL Texture
- NormalizedSkin:尺寸校验、slim 自动判定、64x32 旧格式转 64x64
- LoadedSkin:皮肤加载结果
- RsaKeyUtils:RSA-SHA1withRSA 签名与公钥 PEM
- OfflineSkinServer:内嵌 Yggdrasil 皮肤服务器(7 路由,含 hasJoined 签名)
- HttpServer:新增带 {param} 的模板路由(精确>模板>通配,向后兼容)
- ModSkin: LoadSkinAsync 皮肤加载器(内置/本地文件+披风/CSL 两步协议)
- SkinJson: CSL API 响应解析(default/slim/cape, 容错)
- ModLaunch: Legacy 分支注入 authlib-injector 指向内嵌皮肤服务器;
  导出启动脚本时跳过注入; finally 清理服务器生命周期
- ModProfile: McProfile.Skin 皮肤配置字段 + 序列化/反序列化(兼容旧配置)
- OfflineSkinDialog: 离线皮肤设置对话框(类型/模型/本地文件/披风/CSL URL)
- PageLoginProfileSkin: 离线启用皮肤入口, 打开 OfflineSkinDialog
- PageLaunchLeft: skinLegacy 消费 selectedProfile.Skin 显示自定义头像
- 语言: zh-CN/en-US 新增 Launch.OfflineSkin.* 21 个键
- 接线 NormalizedSkin.IsSlim() 到选文件流程: 自动判定 Steve/Alex 模型(原为死代码, 现符合规格)
- 删除 McLaunchEnd 中与 finally 重复的服务器销毁块
- OfflineSkinDialog 移除 TypeOrder 重复映射, 改读 Combo 项 Tag + Enum.TryParse
- SkinTexture 移除 Has() 双重查询, 统一 Get()
- Skin record 参数 TextureModel 改名 Model(与 LoadedSkin 一致)
- NormalizeCslUrl 改 private
- HttpServer.Start 守卫同时检查模板路由(防模板路由静默跳过)
- OfflineSkinServer 版本号加同步注释
覆盖 SkinTexture 哈希(参考实现一致/透明归一化/缓存)、
NormalizedSkin(尺寸校验/旧格式转换/slim 三分支)、
Skin JSON 序列化(往返/snake_case/容错)
1. 皮肤不生效(根因):HttpServer 拼接 IPv6 前缀时未加方括号(::1),
   导致 HttpListener.AddPrefix 抛异常、OfflineSkinServer 构造失败、注入被静默跳过。
   - 修复:IPv6 地址用 [::1] 包裹
2. Host 头一致性:注入 javaagent 与 textures 载荷的皮肤 URL 统一改用
   127.0.0.1(原 localhost 可能解析为 ::1 导致 Host 头与 HttpListener 前缀不匹配而 404)
3. 头像加载失败:LoadLegacyCustomSkin 对本地文件直接返回原路径,
   但 MySkin.Load 依赖路径含 'Skin\\' 子串解析 skinHeadId,导致缓存目录拼错。
   - 修复:一律导出到 Cache\\Skin\\{uuid}.png 再返回
4. OfflineSkinDialog 布局:根 Grid 显式左对齐固定宽度,消除偏右
5. 新增 OfflineSkinServerTest(4 项):IPv6 构造回归/元数据/hasJoined/404
根因:服务器在 McLaunchStart 的 finally 于游戏窗口出现后即被销毁,
但皮肤/贴图 URL 在进入世界/第三人称时才被游戏拉取,端口已不通。

- 服务器清理改为挂在游戏进程 Exited 事件(进程退出时 Dispose)
- McLaunchStart finally 改为兜底:仅当进程未创建或已退出时清理,
  游戏运行中则交给 Exited 事件
- HttpServer.Dispose 幂等化(进程事件与 finally 兜底可能竞态调用)
原横向 StackPanel + 固定宽度导致左右错位、按钮被挤压、输入框溢出。
改为:左列头像固定 104px、中列固定间距、右列表单自适应;
输入框+按钮用 *+8+92 三列,按钮固定宽度不被压缩;
所有 label 左对齐,垂直间距统一。
用户调整 OfflineSkinDialog.xaml(移除头像预览列)后,代码后置仍引用
ImgPreviewFace/ImgPreviewHair,导致编译失败、应用无法启动。
- 删除 UpdatePreview/GetPreviewPath 及所有调用
- 字段赋默认值消除可空警告
@pcl-ce-automation pcl-ce-automation Bot added 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 size: XXL PR 大小评估:巨型 labels Aug 9, 2026
@sourcery-ai

sourcery-ai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

通过引入皮肤领域模型、内嵌的兼容 Yggdrasil 的 HTTP 皮肤服务器、CSL/本地文件加载工具、用于配置皮肤的 UI,并将该服务器接入启动和档案(配置文件)流程(含完整生命周期管理和测试),为传统(离线)Minecraft 账号增加了完整的离线自定义皮肤支持。

启动过程中注入离线传统皮肤服务器的时序图

sequenceDiagram
    participant Launcher as McLaunch
    participant OfflineSkinServer
    participant AuthlibInjector as authlib_injector.jar
    participant Game as MinecraftProcess

    Launcher->>Launcher: McLaunchOfflineSkinInject(dataList)
    Launcher->>ModSkin: LoadSkinAsync(skin, username)
    ModSkin-->>Launcher: LoadedSkin?
    alt server not created yet
        Launcher->>OfflineSkinServer: new OfflineSkinServer()
    end
    Launcher->>OfflineSkinServer: AddCharacter(uuid, username, loadedSkin)
    Launcher->>OfflineSkinServer: Start()
    Launcher->>Launcher: dataList.Insert(0, "-javaagent:authlib-injector.jar=http://127.0.0.1:{Port} ...")

    Launcher->>Game: McLaunchRun(...) starts gameProcess
    Game->>AuthlibInjector: load javaagent
    AuthlibInjector->>OfflineSkinServer: GET /, /sessionserver/... , /textures/{hash}

    Game-->>Launcher: Exited event
    Launcher->>OfflineSkinServer: Dispose()
    OfflineSkinServer-->>Launcher: Port closed
Loading

File-Level Changes

Change Details Files
引入可复用的皮肤领域模型和纹理工具,用于表示、哈希、归一化、缓存和持久化离线皮肤与披风。
  • 新增 Skin 记录类型、SkinType 和 TextureModel 枚举,以及 InvalidSkinException 和 LoadedSkin,用于建模离线皮肤配置和已加载资源。
  • 实现 SkinTexture 哈希和基于归一化 ARGB 像素的 SHA-256 键的内存缓存,以及 PixelAccess 辅助类和 NormalizedSkin,用于尺寸校验、旧格式转换以及瘦版/Steve 模型检测。
  • 新增 JSON 辅助类 SkinJson,用于解析 Custom Skin Loader API 响应,包括旧字段布局和新字段布局。
  • 新增 RsaKeyUtils,用于生成 RSA 密钥、导出公钥 PEM,以及离线 Yggdrasil 服务器使用的 SHA1withRSA 签名。
PCL.Core/Minecraft/Skin/Skin.cs
PCL.Core/Minecraft/Skin/SkinType.cs
PCL.Core/Minecraft/Skin/TextureModel.cs
PCL.Core/Minecraft/Skin/InvalidSkinException.cs
PCL.Core/Minecraft/Skin/LoadedSkin.cs
PCL.Core/Minecraft/Skin/SkinTexture.cs
PCL.Core/Minecraft/Skin/PixelAccess.cs
PCL.Core/Minecraft/Skin/NormalizedSkin.cs
Plain Craft Launcher 2/Modules/Minecraft/SkinJson.cs
PCL.Core/Minecraft/Skin/RsaKeyUtils.cs
添加一个内嵌的、兼容 Yggdrasil 的离线皮肤 HTTP 服务器,并扩展通用 HttpServer 以支持模板路由和健壮的生命周期语义。
  • 实现 OfflineSkinServer,在回环地址(IPv4/IPv6)上监听,维护 UUID/名字到角色的映射,暴露元数据/状态、档案和 session 端点,并通过 /textures/{hash} 提供缓存纹理,同时返回带 RSA 签名的纹理负载。
  • 扩展 HttpServer,增加 RegisterWithParams 以支持模板路径、模板处理器分发、针对 {param} 段的路径模板匹配、共享的处理器执行核心、IPv6 前缀归一化、具有幂等性的 Start/Dispose 标记,以及在存在精确或模板处理器时都会调用 Init。
PCL.Core/Minecraft/Skin/OfflineSkinServer.cs
PCL.Core/IO/Net/Http/HttpServer.cs
将离线自定义皮肤加载接入启动器:从内置皮肤、本地文件、LittleSkin 或任意 CSL API 加载皮肤,并为传统账号注入由内嵌皮肤服务器支撑的 authlib-injector Java agent。
  • 在 ModSkin 中新增 LoadSkinAsync 及辅助方法,用于加载内置 Steve/Alex 纹理、本地皮肤/披风文件,以及通过解析 SkinJson 和临时下载/缓存来加载 CSL/LittleSkin 纹理。
  • 在 ModLaunch 中引入 OfflineSkinServer 字段,创建 McLaunchOfflineSkinInject 用于启动服务器/注册角色,为传统账号注入带本地服务器 URL 的 authlib-injector javaagent,并在启动 finally 块和游戏进程退出时进行清理。
  • 规范 CSL API URL(协议、去除末尾斜杠),并在出现失败或无效 UUID 时记录日志但不终止启动流程。
Plain Craft Launcher 2/Modules/Minecraft/ModSkin.cs
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs
在档案中持久化离线皮肤配置,并将离线皮肤的使用集成到 UI 中,包括用于配置皮肤的对话框和从自定义皮肤加载头像。
  • 为 McProfile 添加 Skin 属性,通过 Skin.FromStorage 从 profiles.json 加载,并通过 Skin.WriteStorage 以 skin 键保存,未设置时保留为 null。
  • 更新传统皮肤加载逻辑,若配置了自定义离线皮肤则优先使用,将其导出为临时的 Cache/Skin PNG 用于头像/头部渲染;若失败则回退到基于 UUID 的 Steve/Alex。
  • 为离线账号启用编辑按钮,并引入 OfflineSkinDialog(XAML 和代码后端),用于选择本地皮肤/披风文件、通过 NormalizedSkin 自动检测模型、配置 LittleSkin/CSL API,将结果持久化到档案,并刷新启动/档案相关 UI。
  • 将 PageLoginProfileSkin 中皮肤/披风点击事件改为打开离线皮肤对话框,而不是对离线账号显示“暂不支持”的提示。
Plain Craft Launcher 2/Modules/Minecraft/ModProfile.cs
Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.cs
Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfileSkin.xaml.cs
Plain Craft Launcher 2/Pages/PageLaunch/OfflineSkinDialog.xaml
Plain Craft Launcher 2/Pages/PageLaunch/OfflineSkinDialog.xaml.cs
添加测试,以验证皮肤哈希/归一化、序列化和离线皮肤服务器行为,包括 IPv6 前缀构造和 HTTP 端点响应。
  • 创建 SkinTest,覆盖 SkinTexture.ComputeHash(包括透明像素归一化和缓存行为)、NormalizedSkin 的尺寸与瘦版检测逻辑,以及 Skin 存储的往返和错误场景。
  • 创建 OfflineSkinServerTest,验证离线服务器在 IPv6 回环地址上的构造、元数据路由可达性,以及 hasJoined 对已存在和未知玩家的行为。
PCL.Core.Test/Minecraft/Skin/SkinTest.cs
PCL.Core.Test/Minecraft/OfflineSkinServerTest.cs

Possibly linked issues

  • #C#:该 Issue 要求实现离线皮肤功能;本 PR 实现了离线自定义皮肤加载、离线皮肤服务器和档案配置,应直接关联。
  • #C#:本 PR 实现了离线自定义皮肤加载和本地皮肤服务器,是重新加入离线皮肤功能的完整实现。

Tips and commands

Interacting with Sourcery

  • 触发一次新审查: 在 pull request 上评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub Issue: 在某条审查评论下回复,请 Sourcery 从该评论创建一个 issue。也可以直接回复该审查评论并写上 @sourcery-ai issue 来从中创建 issue。
  • 生成 pull request 标题: 在 pull request 标题中任意位置写上 @sourcery-ai,即可随时生成标题。也可以在 pull request 上评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文任意位置写上 @sourcery-ai summary,即可在该位置生成 PR 摘要。也可以在 pull request 上评论 @sourcery-ai summary 来随时(重新)生成摘要。
  • 生成审查者指南: 在 pull request 上评论 @sourcery-ai guide,即可随时(重新)生成审查者指南。
  • 解决所有 Sourcery 评论: 在 pull request 上评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。如果你已经处理完所有评论且不再希望看到它们,这会很有用。
  • 撤销所有 Sourcery 审查: 在 pull request 上评论 @sourcery-ai dismiss,即可撤销所有现有的 Sourcery 审查。尤其适用于你想从一次全新的审查开始——不要忘记再评论 @sourcery-ai review 来触发新审查!

Customizing Your Experience

打开你的 dashboard 来:

  • 启用或禁用审查功能,例如 Sourcery 生成的 pull request 摘要、审查者指南等。
  • 更改审查语言。
  • 添加、删除或编辑自定义审查说明。
  • 调整其他审查设置。

Getting Help

Original review guide in English

Reviewer's Guide

Adds full offline custom skin support for legacy (offline) Minecraft accounts by introducing a skin domain model, an embedded Yggdrasil-compatible HTTP skin server, CSL/local-file loading utilities, UI for configuring skins, and wiring the server into the launch and profile flows with appropriate lifecycle management and tests.

Sequence diagram for offline legacy skin server injection during launch

sequenceDiagram
    participant Launcher as McLaunch
    participant OfflineSkinServer
    participant AuthlibInjector as authlib_injector.jar
    participant Game as MinecraftProcess

    Launcher->>Launcher: McLaunchOfflineSkinInject(dataList)
    Launcher->>ModSkin: LoadSkinAsync(skin, username)
    ModSkin-->>Launcher: LoadedSkin?
    alt server not created yet
        Launcher->>OfflineSkinServer: new OfflineSkinServer()
    end
    Launcher->>OfflineSkinServer: AddCharacter(uuid, username, loadedSkin)
    Launcher->>OfflineSkinServer: Start()
    Launcher->>Launcher: dataList.Insert(0, "-javaagent:authlib-injector.jar=http://127.0.0.1:{Port} ...")

    Launcher->>Game: McLaunchRun(...) starts gameProcess
    Game->>AuthlibInjector: load javaagent
    AuthlibInjector->>OfflineSkinServer: GET /, /sessionserver/... , /textures/{hash}

    Game-->>Launcher: Exited event
    Launcher->>OfflineSkinServer: Dispose()
    OfflineSkinServer-->>Launcher: Port closed
Loading

File-Level Changes

Change Details Files
Introduce a reusable skin domain model and texture utilities to represent, hash, normalize, cache, and persist offline skins and capes.
  • Add Skin record, SkinType and TextureModel enums plus InvalidSkinException and LoadedSkin to model offline skin configuration and loaded assets.
  • Implement SkinTexture hashing and in-memory cache keyed by SHA-256 of normalized ARGB pixels, plus PixelAccess helpers and NormalizedSkin for size validation, old-format conversion, and slim/Steve detection.
  • Add JSON helper SkinJson to parse Custom Skin Loader API responses including old and new field layouts.
  • Add RsaKeyUtils for RSA key generation, public key PEM export, and SHA1withRSA signing used by the offline Yggdrasil server.
PCL.Core/Minecraft/Skin/Skin.cs
PCL.Core/Minecraft/Skin/SkinType.cs
PCL.Core/Minecraft/Skin/TextureModel.cs
PCL.Core/Minecraft/Skin/InvalidSkinException.cs
PCL.Core/Minecraft/Skin/LoadedSkin.cs
PCL.Core/Minecraft/Skin/SkinTexture.cs
PCL.Core/Minecraft/Skin/PixelAccess.cs
PCL.Core/Minecraft/Skin/NormalizedSkin.cs
Plain Craft Launcher 2/Modules/Minecraft/SkinJson.cs
PCL.Core/Minecraft/Skin/RsaKeyUtils.cs
Add an embedded offline Yggdrasil-compatible skin HTTP server and extend the generic HttpServer to support template routes and robust lifecycle semantics.
  • Implement OfflineSkinServer that listens on loopback (IPv4/IPv6), maintains a UUID/name-to-character map, exposes metadata/status, profile and session endpoints, and serves cached textures via /textures/{hash} with RSA-signed textures payloads.
  • Extend HttpServer with RegisterWithParams for template paths, template handler dispatch, a path-template matcher for {param} segments, shared handler execution core, IPv6 prefix normalization, idempotent Start/Dispose flags, and Init invocation when either exact or template handlers exist.
PCL.Core/Minecraft/Skin/OfflineSkinServer.cs
PCL.Core/IO/Net/Http/HttpServer.cs
Wire offline custom skin loading into the launcher: load skins from built-in, local files, LittleSkin, or arbitrary CSL APIs, and inject an authlib-injector Java agent backed by the embedded skin server for Legacy accounts.
  • Add ModSkin.LoadSkinAsync and helpers to load built-in Steve/Alex textures, local skin/cape files, and CSL/LittleSkin textures via SkinJson parsing and temporary download/caching.
  • Introduce OfflineSkinServer field in ModLaunch, create McLaunchOfflineSkinInject to start/register characters, inject authlib-injector javaagent with local server URL for Legacy accounts, and add cleanup in launch finally and on game process exit.
  • Normalize CSL API URLs (scheme, trimming trailing slash) and log failures or invalid UUIDs without aborting the launch.
Plain Craft Launcher 2/Modules/Minecraft/ModSkin.cs
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs
Persist offline skin configuration in profiles and integrate offline skin usage into the UI, including a dialog to configure skins and avatar loading from custom skins.
  • Add Skin property to McProfile, load it from profiles.json via Skin.FromStorage, and save it via Skin.WriteStorage under the skin key, preserving null when unset.
  • Update legacy skin loader to use custom offline skin if configured, exporting to a temporary Cache/Skin PNG for avatar/head rendering, with fallback to UUID-based Steve/Alex on failure.
  • Enable edit button for offline accounts and introduce OfflineSkinDialog with XAML and code-behind to select local skin/cape files, auto-detect model via NormalizedSkin, configure LittleSkin/CSL API, persist to profile, and refresh launch/profile UI.
  • Change skin/cape click handlers in PageLoginProfileSkin to open the offline skin dialog instead of showing unsupported hints for offline accounts.
Plain Craft Launcher 2/Modules/Minecraft/ModProfile.cs
Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.cs
Plain Craft Launcher 2/Pages/PageLaunch/PageLoginProfileSkin.xaml.cs
Plain Craft Launcher 2/Pages/PageLaunch/OfflineSkinDialog.xaml
Plain Craft Launcher 2/Pages/PageLaunch/OfflineSkinDialog.xaml.cs
Add tests to validate skin hashing/normalization, serialization, and offline skin server behavior, including IPv6 prefix construction and HTTP endpoint responses.
  • Create SkinTest to cover SkinTexture.ComputeHash (including transparent-pixel normalization and cache behavior), NormalizedSkin size and slim detection logic, and Skin storage round-trips and error cases.
  • Create OfflineSkinServerTest to assert offline server construction with IPv6 loopback, metadata route reachability, and hasJoined behavior for existing and unknown players.
PCL.Core.Test/Minecraft/Skin/SkinTest.cs
PCL.Core.Test/Minecraft/OfflineSkinServerTest.cs

Possibly linked issues

  • #C#: Issue要求离线皮肤功能;PR实现离线自定义皮肤加载、离线皮肤服务器和档案配置,应直接关联。
  • #C#: PR实现离线自定义皮肤加载和本地皮肤服务器,正是重新加入离线皮肤功能的完整实现。

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了一个问题,并给出了一些整体反馈:

  • 在 HttpServer.Stop/Dispose 中你从未重置 _started 标志,因此已停止的服务器无法再次启动;建议在 Stop 中清除 _started,以保证可安全复用,并与新的「可重复调用 Start」语义保持一致。
  • LoadLegacyCustomSkin 会将临时 PNG 写入 Cache\Skin 目录,但从不删除它们,这可能会导致离线账户的缓存文件无限增长;建议在保存或重新加载皮肤时添加一个简单的清理策略(例如覆盖或清理旧的缓存条目)。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- 在 HttpServer.Stop/Dispose 中你从未重置 `_started` 标志,因此已停止的服务器无法再次启动;建议在 Stop 中清除 `_started`,以保证可安全复用,并与新的「可重复调用 Start」语义保持一致。
- LoadLegacyCustomSkin 会将临时 PNG 写入 `Cache\Skin` 目录,但从不删除它们,这可能会导致离线账户的缓存文件无限增长;建议在保存或重新加载皮肤时添加一个简单的清理策略(例如覆盖或清理旧的缓存条目)。

## Individual Comments

### Comment 1
<location path="PCL.Core/IO/Net/Http/HttpServer.cs" line_range="21-25" />
<code_context>
     private readonly Dictionary<(HttpMethod method, string path), Func<HttpListenerRequest, Task<HttpRouteResponse>>> _handlers = new();
+    private readonly Dictionary<(HttpMethod method, string path), Func<HttpListenerRequest, IReadOnlyDictionary<string, string>, Task<HttpRouteResponse>>> _templateHandlers = new();
     private bool _initialized = false;
+    private bool _started = false;
+    private bool _disposed = false;

</code_context>
<issue_to_address>
**issue (bug_risk):** Reset `_started` when stopping to keep `Start()`/`Stop()` semantics consistent.

`_started``Start()` 中被设置为 `true`,但在 `Stop()` 中从未被清除。调用 `Stop()` 后,对同一实例再次调用 `Start()` 会提前返回,并且不会重新启动 `HttpListener`,这与「重复调用安全」保证相矛盾,并破坏了复用场景。请在 `_server.Stop()` 成功后于 `Stop()` 中重置 `_started = false;`,并考虑在未启动时对 `Stop()` 调用进行保护。
</issue_to_address>

Sourcery 对开源项目免费——如果你觉得我们的代码评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续评审。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • In HttpServer.Stop/Dispose you never reset the _started flag, so a stopped server cannot be started again; consider clearing _started in Stop to make reuse safe and align with the new "repeat Start" semantics.
  • LoadLegacyCustomSkin writes temporary PNGs to Cache\Skin but never deletes them, which can lead to unbounded growth of cached files for offline accounts; consider adding a simple cleanup strategy (e.g., overwriting or pruning old cache entries) when saving or reloading skins.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In HttpServer.Stop/Dispose you never reset the `_started` flag, so a stopped server cannot be started again; consider clearing `_started` in Stop to make reuse safe and align with the new "repeat Start" semantics.
- LoadLegacyCustomSkin writes temporary PNGs to `Cache\Skin` but never deletes them, which can lead to unbounded growth of cached files for offline accounts; consider adding a simple cleanup strategy (e.g., overwriting or pruning old cache entries) when saving or reloading skins.

## Individual Comments

### Comment 1
<location path="PCL.Core/IO/Net/Http/HttpServer.cs" line_range="21-25" />
<code_context>
     private readonly Dictionary<(HttpMethod method, string path), Func<HttpListenerRequest, Task<HttpRouteResponse>>> _handlers = new();
+    private readonly Dictionary<(HttpMethod method, string path), Func<HttpListenerRequest, IReadOnlyDictionary<string, string>, Task<HttpRouteResponse>>> _templateHandlers = new();
     private bool _initialized = false;
+    private bool _started = false;
+    private bool _disposed = false;

</code_context>
<issue_to_address>
**issue (bug_risk):** Reset `_started` when stopping to keep `Start()`/`Stop()` semantics consistent.

`_started` is set to `true` in `Start()` but never cleared in `Stop()`. After calling `Stop()`, a subsequent `Start()` on the same instance will return early and not restart the `HttpListener`, contradicting the "重复调用安全" guarantee and breaking reuse scenarios. Please reset `_started = false;` in `Stop()` after `_server.Stop()` succeeds, and consider guarding `Stop()` against being called when not started.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +21 to 25
private bool _started = false;
private bool _disposed = false;

protected HttpServer(IPAddress[] listenAddr, ushort port = 0)
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Reset _started when stopping to keep Start()/Stop() semantics consistent.

_startedStart() 中被设置为 true,但在 Stop() 中从未被清除。调用 Stop() 后,对同一实例再次调用 Start() 会提前返回,并且不会重新启动 HttpListener,这与「重复调用安全」保证相矛盾,并破坏了复用场景。请在 _server.Stop() 成功后于 Stop() 中重置 _started = false;,并考虑在未启动时对 Stop() 调用进行保护。

Original comment in English

issue (bug_risk): Reset _started when stopping to keep Start()/Stop() semantics consistent.

_started is set to true in Start() but never cleared in Stop(). After calling Stop(), a subsequent Start() on the same instance will return early and not restart the HttpListener, contradicting the "重复调用安全" guarantee and breaking reuse scenarios. Please reset _started = false; in Stop() after _server.Stop() succeeds, and consider guarding Stop() against being called when not started.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6da5f66f1e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

{
// 仅当退出的正是当前记录的游戏进程时才关闭服务器:
// 多开时先退出的游戏不能关掉仍在服务后一局游戏的服务器
if (ReferenceEquals(mcLaunchProcess, gameProcess) && mcLaunchOfflineSkinServer is not null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the skin server until every launched game exits

When a second launch happens while an earlier custom-skin game is still running, mcLaunchProcess is overwritten with the newer process. If that newer process exits first (or is killed after a launch cancel), this handler disposes the shared mcLaunchOfflineSkinServer, so the older game loses the local Yggdrasil/texture URLs it may still request after entering worlds or reloading skins; track all processes using the server and only dispose it when none remain.

Useful? React with 👍 / 👎.

@MoYuan-CN

MoYuan-CN commented Aug 9, 2026

Copy link
Copy Markdown
Member

在合并前得给 UI 先改了
这什么雷霆 Dialog 对话窗()

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2150c4b60a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +286 to +287
if (skinTex is null)
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return cape-only CSL profiles

When a LittleSkin/CSL API response has a cape hash but no skin hash, this downloads capeTex and then discards the whole result because skinTex is null. The offline skin server emits CAPE independently of SKIN, and local-file mode already permits cape-only configurations, so users with only a cape configured get an empty texture profile instead of their cape; return a LoadedSkin whenever either texture is present.

Useful? React with 👍 / 👎.

@sysfox

sysfox commented Aug 9, 2026

Copy link
Copy Markdown
Author
image image

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f08a2f3ca2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

private static async Task<SkinTexture?> DownloadCslTextureAsync(string api, string hash)
{
var directory = Path.Combine(ModBase.pathTemp, "Skin");
var tempPath = Path.Combine(directory, hash + ".png");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate CSL texture hashes before using them as paths

When a configured CSL/LittleSkin endpoint returns a texture hash containing path separators or an absolute path, this Path.Combine lets tempPath escape the intended temp skin directory; FileDownloader.DownloadAsync cleans up the target path before downloading, so a malicious or compromised skin API can delete or overwrite arbitrary .png paths reachable by the launcher. Please reject non-hash characters or map the remote hash to a safe local filename before using it as a path component.

Useful? React with 👍 / 👎.

@Chiloven945 Chiloven945 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

考虑参考现有 UI 体系对目前 UI 进行修整。PageLaunchLeft 有既有可以参考的设计,例如多个输入框堆叠的形式。

Image Image

@sysfox

sysfox commented Aug 9, 2026

Copy link
Copy Markdown
Author

考虑参考现有 UI 体系对目前 UI 进行修整。PageLaunchLeft 有既有可以参考的设计,例如多个输入框堆叠的形式。

Image Image

可以,我在设计的时候就是想不出来到底怎么排版

<sys:String x:Key="Launch.OfflineSkin.SelectCape">选择披风文件…</sys:String>
<sys:String x:Key="Launch.OfflineSkin.CslApiUrl">CSL API 地址</sys:String>
<sys:String x:Key="Launch.OfflineSkin.LittleSkinHint">LittleSkin 为 Minecraft 玩家提供皮肤托管服务。</sys:String>
<sys:String x:Key="Launch.OfflineSkin.OpenLittleSkin">打开其官网</sys:String>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

删掉 “其” 更通顺一些?

@sysfox

sysfox commented Aug 11, 2026

Copy link
Copy Markdown
Author
image image image

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc4aec2c3d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +85 to +86
if (!string.IsNullOrEmpty(GetString(textures, "slim")))
return TextureModel.Slim;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect CSL texture preference order

When a CustomSkinAPI response includes both default and slim textures, this always returns Slim because it checks slim first. The CustomSkinAPI spec says the texture dictionary order is the user's preference order, so users whose server returns default before slim will still be launched/rendered with the slim skin/model; iterate the textures entries in JSON order and choose the first supported skin model instead.

Useful? React with 👍 / 👎.

@Chiloven945 Chiloven945 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确保选择文件夹的按钮和其他控件的圆角统一?

@sysfox

sysfox commented Aug 12, 2026

Copy link
Copy Markdown
Author

CornerRadius已经改为2
image

@sysfox
sysfox requested a review from Chiloven945 August 12, 2026 01:40

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36ef7a282f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +959 to +960
data.output = skinConfig is not null && skinConfig.Type != SkinType.Default
? LoadLegacyCustomSkin(skinConfig, data.input[0])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Drop aborted legacy-skin results before publishing

When a CSL/LittleSkin avatar load is superseded, such as switching profiles or saving another skin while the network request is still in flight, LoaderTask.Start(isForceRestart: true) only marks the old task aborted; this branch still blocks in LoadLegacyCustomSkin, then writes data.output and proceeds to Skin.Load() because the later check only compares the shared static loader. That lets a stale profile's skin overwrite the current avatar/cache, so check data.IsAborted after the load before publishing the result, as the Ms/Auth loaders do.

Useful? React with 👍 / 👎.

Comment thread PCL.Core/App/Localization/Languages/en-US.xaml Outdated
Co-authored-by: ChilovenBustiangle <116699482+Chiloven945@users.noreply.github.com>
@sysfox

sysfox commented Aug 13, 2026

Copy link
Copy Markdown
Author
image

@sysfox
sysfox requested a review from Chiloven945 August 13, 2026 10:22

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ff5e73bbb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

private void UpdateConfirmEnabled()
{
BtnConfirm.IsEnabled = CurrentType != SkinType.CustomSkinLoaderApi ||
Uri.TryCreate(TextCslApi.Text.Trim(), UriKind.Absolute, out _);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept scheme-less CSL API URLs

When a user enters a CSL API base such as littleskin.cn/csl, this check disables OK and the confirm handler rejects the value because UriKind.Absolute requires an explicit scheme, even though ModSkin.NormalizeCslUrl is written to support exactly this input by prepending https://. That makes supported CustomSkinLoaderApi URLs impossible to save from the new UI; validate using the same normalization path or allow scheme-less hostnames before blocking confirmation.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: XXL PR 大小评估:巨型 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants