Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,17 @@ extra wrapper directory, enter it first; the expected filenames must be
immediate children of the CLI path or the narrow GUI selection described above.

If the 3DS `system` file is omitted, a core `user#` conversion cannot migrate
the housekeeper's gallery/movie unlock history. `system` is shared across all
three character slots and also contains settings unrelated to the selected
slot. Therefore `convert-system` requires both the 3DS source and an existing,
initialized Cemu target. It bitwise-unions only the verified gallery/movie
flag range (Cemu file offsets `0x68..0x77`) and preserves every other target
byte. It refuses a new/missing target instead of replacing all shared data.
the housekeeper's gallery/movie unlock history. The physical save layout
exposes one title-wide `system` rather than separate `system1`, `system2`, and
`system3` files. Current evidence cannot attribute individual flags inside it
to `user1`, `user2`, or `user3`, and it also contains settings unrelated to the
selected slot. Therefore `convert-system` requires both the 3DS source and an
existing, initialized Cemu target. It
bitwise-unions only the currently mapped gallery/movie flag range (Cemu file
offsets `0x68..0x77`) and preserves every other target byte. It refuses a
new/missing target instead of replacing all shared data. This conservative
transaction is not a claim that every `system` bit has official-transfer or
game-runtime parity.

### Legacy Wii U save-editor caveat

Expand All @@ -218,6 +223,24 @@ with such a tool can turn its pigment black even when the converted talisman is
valid. These editor behaviors must not be "fixed" by clamping or rewriting
otherwise valid converted equipment records.

### Cha-Cha / Kayamba mask proficiency

The companion mask/mastery area in `user#` is a packed byte block, not a table
of uniformly endian-swapped `u16` values. Current conversion preserves the
confirmed packed range exactly while converting only the numeric prefix. This
keeps the mask screen, companion status screen, and post-quest serializer on
the same representation. Compatibility repair can also recognize and repair
the historical two-byte swap at relative companion offset `0xE4` without
replacing unrelated Wii U progress.

### Monster guide discovery

Monster discovery is controlled by the source 3DS discovery/crown state byte,
not inferred from slay or capture counters. Current conversion applies the
same state-only bit mapping to the personal record, received guild cards, and
CEC/offline-hall partners. This prevents historical conversions from turning
hidden rows with non-zero counters into extra monster-guide pages.

### Before you write: paths, inspection, and dry-run

The examples below run from this repository after Rust is installed. Define a
Expand Down
10 changes: 9 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,20 @@ Windows 前置条件、行为和可选参数见
OutBox__/... <- 本机猎人的发出广播
```

如果没有提供 3DS `system` 文件,单独转换核心 `user#` 无法迁移管家处可查看的画廊/动画解锁记录。`system` 由三个角色槽位共同使用,还包含与当前角色无关的设置。因此 `convert-system` 必须同时提供 3DS 源文件和已初始化、已存在的 Cemu 目标文件。它只会对已确认的画廊/动画标记范围(Cemu 文件偏移 `0x68..0x77`)逐位取并集,其他目标字节全部保留;若目标不存在则拒绝执行,不会整文件覆盖共享数据。
如果没有提供 3DS `system` 文件,单独转换核心 `user#` 无法迁移管家处可查看的画廊/动画解锁记录。物理存档布局只有一个标题级共享 `system`,不存在独立的 `system1`、`system2`、`system3` 文件;现有证据还不能把其中每个具体标记可靠归因到 `user1`、`user2` 或 `user3`,而且它还包含与当前角色无关的设置。因此 `convert-system` 必须同时提供 3DS 源文件和已初始化、已存在的 Cemu 目标文件。它只会对当前已映射的画廊/动画标记范围(Cemu 文件偏移 `0x68..0x77`)逐位取并集,其他目标字节全部保留;若目标不存在则拒绝执行,不会整文件覆盖共享数据。这个保守事务并不代表 `system` 的每一个标记都已经与官方迁移或游戏运行态完全对齐

### 旧版 Wii U 存档修改器注意事项

护石技能点在装备记录中是一个有符号单字节值。部分旧修改器却按无符号字节读取,因此合法的 `-5` 会显示成 `251`。转换器会保留该原始有符号字节,只转换确实存在平台字节序差异的字段。部分旧修改器在重写防具时还会重新构造防具头,却不保留其中的 RGB 染色字节;用这种修改器编辑防具可能让防具统一变黑,即使转换后的护石本身完全合法。不能通过钳制技能点或重写原本正确的装备记录来“修复”这类修改器问题。

### 茶茶/卡扬巴面具熟练度

`user#` 中的随从面具/熟练度区域是按字节打包的数据块,并不是一张全部按 `u16` 交换字节序的表。当前转换只处理已经证实的数值前缀,完整保留已确认的打包区间,使面具界面、随从属性界面与任务结算回写使用同一种表示。兼容修复也可以识别并修复历史版本在随从相对偏移 `0xE4` 处造成的两字节误交换,同时保留无关的 Wii U 后续进度。

### 怪物图鉴发现状态

怪物是否出现在图鉴中只由 3DS 源存档的发现/大小冠状态字节决定,不能根据讨伐数或捕获数推断。当前转换对个人记录、收到的公会名片以及 CEC/离线集会所伙伴统一使用同一份“仅状态字节”映射,避免旧逻辑把“计数非零但尚未发现”的记录错误扩展成额外图鉴页。

如果解压后的目录中直接包含 `user2`,使用 CLI 转换核心槽位时必须传入这个**文件**。在原生工作台中,选择该目录并选中 `user2` 时,只会解析其直接子文件 `user2`。如果目录中直接包含 `card1` 到 `quest4`,该目录只能作为 `convert-extras` 输入。如果解压结果外面还有一层包装目录,需要先进入这一层;上述预期文件必须是 CLI 路径或上述受限 GUI 选择的直接子项。

### 写入前:设置路径、检查和 dry-run
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public enum ConverterCopy {
"Components.QuestsDetail": "迁移任务数据。",
"Components.SystemSource": "3DS system",
"Components.SystemTarget": "Cemu system",
"Components.SystemWarning": "system 由三个角色槽位共享。必须同时提供 3DS 源文件和已初始化的 Cemu 目标;转换器只合并管家画廊/动画标记,其他 Cemu 设置与跨槽位数据保持不变。",
"Components.SystemWarning": "物理存档只有一个共享 system,不存在独立的 system1、system2、system3;目前无法把具体画廊/动画标记可靠归因到某个角色槽。必须同时提供 3DS 源文件和已初始化的 Cemu 目标;转换器只修改狭窄的已映射范围,其他 Cemu 字节保持不变。精确官方迁移/运行态一致性仍未验证。",
"Components.SystemFooter": "目标必须是已存在且格式有效的 Cemu system。若不提供这一对文件,则不会迁移管家画廊/动画记录。",
"Components.SystemSourceMessage": "请选择一个 3DS system 文件。",
"Components.SystemTargetMessage": "请选择一个 Cemu system 文件。",
Expand Down Expand Up @@ -417,7 +417,7 @@ public enum ConverterCopy {
"Components.QuestsDetail": "Migrate quest data.",
"Components.SystemSource": "3DS system",
"Components.SystemTarget": "Cemu system",
"Components.SystemWarning": "System is shared by all three character slots. Supply both the 3DS source and an initialized Cemu target; only housekeeper gallery/movie flags are merged while every other Cemu setting and shared-slot byte is retained.",
"Components.SystemWarning": "The physical save has one shared system file rather than separate system1, system2, and system3 files. Individual gallery/movie flags cannot currently be attributed reliably to a character slot. Supply both the 3DS source and an initialized Cemu target; the converter changes only its narrow mapped range and preserves every other Cemu byte. Exact official/runtime parity remains unverified.",
"Components.SystemFooter": "The target must be an existing valid Cemu system. If this pair is omitted, housekeeper gallery/movie history is not migrated.",
"Components.SystemSourceMessage": "Choose exactly one 3DS system file.",
"Components.SystemTargetMessage": "Choose exactly one Cemu system file.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"Repair.Version.Hint" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Use automatic detection if the version is unknown. If several historical versions match, Dry Run will require an explicit selection before it can authorize a write." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "不知道版本时先自动检测;若多个历史版本都匹配,Dry Run 会要求你明确选择后再运行。" } } } },
"Repair.Version.Required" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Several versions match this save. Select the converter version originally used, then run Dry Run again." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "检测到多个可能版本。请选择当时使用的转换器版本,然后重新运行 Dry Run。" } } } },
"Repair.PreviewSHA256" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Repair preview SHA-256" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "修复预览 SHA-256" } } } },
"Components.SystemWarning" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "System is shared by all three character slots. Supply both the 3DS source and an initialized Cemu target; only housekeeper gallery/movie flags are merged while every other Cemu setting and shared-slot byte is retained." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "system 由三个角色槽位共享。必须同时提供 3DS 源文件和已初始化的 Cemu 目标;转换器只合并管家画廊/动画标记,其他 Cemu 设置与跨槽位数据保持不变。" } } } },
"Components.SystemWarning" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "The physical save has one shared system file rather than separate system1, system2, and system3 files. Individual gallery/movie flags cannot currently be attributed reliably to a character slot. Supply both the 3DS source and an initialized Cemu target; the converter changes only its narrow mapped range and preserves every other Cemu byte. Exact official/runtime parity remains unverified." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "物理存档只有一个共享 system,不存在独立的 system1、system2、system3;目前无法把具体画廊/动画标记可靠归因到某个角色槽。必须同时提供 3DS 源文件和已初始化的 Cemu 目标;转换器只修改狭窄的已映射范围,其他 Cemu 字节保持不变。精确官方迁移/运行态一致性仍未验证。" } } } },
"Guide.ComponentsReady" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Optional data is set. Next, run a read-only Dry Run to review what this conversion will write." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "可选数据已确认。接下来运行只读 Dry Run,确认本次转换会写入什么。" } } } },
"Guide.CoreDryRunCompleteWithOptionals" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "The core-slot Dry Run passed. Continue to the write page; selected system or ExtData data still needs its own Dry Run and write." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "核心角色 Dry Run 已通过。接下来进入写入页;已选 system 或 ExtData 仍须分别 Dry Run 和写入。" } } } },
"Guide.DryRunComplete" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "Dry Run passed. No save has been modified; continue when you are ready to confirm the write." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Dry Run 已通过。确认写入前不会修改任何存档。" } } } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ private string Text(string key)
[nameof(SharedSystem)] = "Shared system",
[nameof(SystemSource)] = "3DS system file",
[nameof(SystemTarget)] = "Existing initialized Cemu system file",
[nameof(SystemMergeWarning)] = "System is shared by all three character slots and may contain settings or data for the other slots. Supply both the 3DS source and the existing Cemu target. The converter merges only housekeeper gallery/movie flags and preserves every other Cemu byte.",
[nameof(SystemMergeWarning)] = "The title has one shared system file, not separate user1, user2, and user3 files. Individual gallery/movie flags cannot currently be attributed to a character slot. Supply both the 3DS source and existing Cemu target; the converter changes only its narrow mapped range and preserves every other Cemu byte. Exact official/runtime parity remains unverified.",
[nameof(RunSystemDryRun)] = "Run system Dry Run",
[nameof(WriteSystem)] = "Merge gallery/movie flags",
[nameof(SystemManifest)] = "System manifest",
Expand Down Expand Up @@ -611,7 +611,7 @@ private string Text(string key)
[nameof(SharedSystem)] = "共享 system",
[nameof(SystemSource)] = "3DS system 文件",
[nameof(SystemTarget)] = "已存在且已初始化的 Cemu system 文件",
[nameof(SystemMergeWarning)] = "system 由三个角色槽位共享,可能同时保存其他槽位数据或设置。必须同时提供 3DS 源 system 和现有 Cemu 目标 system;转换器只合并管家画廊/动画标记,其他 Cemu 字节全部保持不变。",
[nameof(SystemMergeWarning)] = "整个游戏标题只有一个共享 system 文件,并不存在独立的 user1、user2、user3 system;当前也无法把具体画廊/动画标记可靠归因到某个角色槽。必须同时提供 3DS 源 system 和现有 Cemu 目标 system;转换器只修改狭窄的已映射范围,其他 Cemu 字节全部保持不变。精确官方迁移/运行态一致性仍未验证。",
[nameof(RunSystemDryRun)] = "运行 system Dry Run",
[nameof(WriteSystem)] = "合并画廊/动画标记",
[nameof(SystemManifest)] = "system manifest",
Expand Down
8 changes: 7 additions & 1 deletion apps/mh3g-save-converter-windows/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,13 @@
<ScrollViewer><StackPanel MaxWidth="900" HorizontalAlignment="Center" Spacing="16" Padding="0,18,0,24">
<Grid><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><StackPanel Spacing="6"><TextBlock Text="{Binding Copy.DryRunPageTitle}" Style="{StaticResource PageTitleTextStyle}" AutomationProperties.AutomationId="mh3g.converter.windows.page.dryRun.title" /><TextBlock Text="{Binding Copy.DryRunDescription}" Style="{StaticResource SectionCaptionTextStyle}" /></StackPanel><TextBlock Grid.Column="1" Text="{Binding Copy.StepThree}" /></Grid>
<Border Style="{StaticResource WorkSurfaceStyle}"><StackPanel Spacing="12"><InfoBar IsOpen="True" Visibility="{Binding DryRunReadyVisibility}" IsClosable="False" Severity="Success" Message="{Binding Copy.DryRunReadyMessage}" AutomationProperties.AutomationId="mh3g.converter.windows.state.dryRunReady" /><TextBlock Text="{Binding SourcePathPreview}" /><TextBlock Text="{Binding CurrentPathPreview}" Visibility="{Binding RepairCurrentVisibility}" /><TextBlock Text="{Binding TargetPathPreview}" /><ProgressBar IsIndeterminate="True" Visibility="{Binding BusyVisibility}" AutomationProperties.AutomationId="mh3g.converter.windows.state.running" /></StackPanel></Border>
<Expander Header="{Binding Copy.AdvancedDetails}"><TextBlock Text="{Binding LatestReport}" FontFamily="Cascadia Mono" TextWrapping="Wrap" IsTextSelectionEnabled="True" /></Expander>
<Expander Header="{Binding Copy.AdvancedDetails}" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" AutomationProperties.AutomationId="mh3g.converter.windows.details.dryRun">
<StackPanel Spacing="10">
<TextBlock Text="{Binding Copy.ResultEmpty}" TextWrapping="Wrap" Foreground="{ThemeResource TextFillColorSecondaryBrush}" Visibility="{Binding LatestReportEmptyVisibility}" AutomationProperties.AutomationId="mh3g.converter.windows.details.dryRun.empty" />
<InfoBar IsOpen="{Binding HasLatestError}" Visibility="{Binding ResultFailureVisibility}" IsClosable="False" Severity="Error" Title="{Binding Copy.Failed}" Message="{Binding LatestError}" AutomationProperties.AutomationId="mh3g.converter.windows.details.dryRun.error" />
<TextBox Text="{Binding LatestReport, Mode=OneWay}" IsReadOnly="True" AcceptsReturn="True" TextWrapping="NoWrap" FontFamily="Cascadia Mono" MaxHeight="320" ScrollViewer.VerticalScrollBarVisibility="Auto" ScrollViewer.HorizontalScrollBarVisibility="Auto" Visibility="{Binding LatestReportVisibility}" AutomationProperties.AutomationId="mh3g.converter.windows.details.dryRun.report" />
</StackPanel>
</Expander>
</StackPanel></ScrollViewer>
<Border Grid.Row="1" Style="{StaticResource WorkSurfaceStyle}" Padding="16,12"><Grid ColumnSpacing="12"><Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions><TextBlock Text="{Binding Copy.DryRunNeedsInspect}" Visibility="{Binding DryRunBlockedVisibility}" VerticalAlignment="Center" AutomationProperties.LiveSetting="Polite" AutomationProperties.AutomationId="mh3g.converter.windows.state.dryRunBlocked" /><Button Grid.Column="1" Click="DryRun_Click" IsEnabled="{Binding CanRunCoreDryRun}" Content="{Binding Copy.RunDryRun}" Style="{StaticResource AccentButtonStyle}" AutomationProperties.AutomationId="mh3g.converter.windows.action.runDryRun" /></Grid></Border>
</Grid>
Expand Down
23 changes: 18 additions & 5 deletions apps/mh3g-save-converter-windows/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -499,18 +499,31 @@ private void RaiseFixtureLiveRegion()

private void SkipOptional_Click(object sender, RoutedEventArgs e)
{
if (ViewModel.CommitRepairOptionalScope(skip: true))
{
ShowConvertStep(ConvertStep.DryRun);
}
ContinueFromOptional(skip: true);
}

private void ContinueToDryRun_Click(object sender, RoutedEventArgs e)
{
if (ViewModel.CommitRepairOptionalScope(skip: false))
ContinueFromOptional(skip: false);
}

private void ContinueFromOptional(bool skip)
{
if (!ViewModel.CommitRepairOptionalScope(skip))
{
return;
}

if (ViewModel.CanRunCoreDryRun)
{
ShowConvertStep(ConvertStep.DryRun);
return;
}

// Defensive recovery: never expose the Dry Run page with a disabled
// action after an unexpected state mismatch. The input page carries
// the actionable inspection gate.
ShowConvertStep(ConvertStep.Input);
}

private void StartConversion_Click(object sender, RoutedEventArgs e)
Expand Down
Loading
Loading