feat(quests): support JSON quest files alongside .bin

Adds human-readable JSON as an alternative quest format for bin/quests/.
The server tries .bin first (full backward compatibility), then falls back
to .json and compiles it to the MHF binary wire format on the fly.

JSON quests cover all documented fields: text (UTF-8 → Shift-JIS),
objectives (all 12 types), monster spawns, reward tables, supply box,
stages, rank requirements, variant flags, and forced equipment.

Also adds ParseQuestBinary for the reverse direction, enabling tools to
round-trip quests and verify that JSON-compiled output is bit-for-bit
identical to a hand-authored .bin for the same quest data.

49 tests: compiler, parser, 13 round-trip scenarios, and golden byte-
level assertions covering every section of the binary layout.
This commit is contained in:
Houmgaor
2026-03-19 17:56:50 +01:00
parent 0911d15709
commit c64260275b
4 changed files with 1949 additions and 3 deletions

View File

@@ -126,9 +126,9 @@ func handleMsgSysGetFile(s *Session, p mhfpacket.MHFPacket) {
pkt.Filename = seasonConversion(s, pkt.Filename)
}
data, err := os.ReadFile(filepath.Join(s.server.erupeConfig.BinPath, fmt.Sprintf("quests/%s.bin", pkt.Filename)))
data, err := loadQuestBinary(s, pkt.Filename)
if err != nil {
s.logger.Error("Failed to open quest file", zap.String("binPath", s.server.erupeConfig.BinPath), zap.String("filename", pkt.Filename))
s.logger.Error("Failed to open quest file", zap.String("binPath", s.server.erupeConfig.BinPath), zap.String("filename", pkt.Filename), zap.Error(err))
doAckBufFail(s, pkt.AckHandle, nil)
return
}
@@ -140,10 +140,34 @@ func handleMsgSysGetFile(s *Session, p mhfpacket.MHFPacket) {
}
func questFileExists(s *Session, filename string) bool {
_, err := os.Stat(filepath.Join(s.server.erupeConfig.BinPath, fmt.Sprintf("quests/%s.bin", filename)))
base := filepath.Join(s.server.erupeConfig.BinPath, "quests", filename)
if _, err := os.Stat(base + ".bin"); err == nil {
return true
}
_, err := os.Stat(base + ".json")
return err == nil
}
// loadQuestBinary loads a quest file by name, trying .bin first then .json.
// For .json files it compiles the JSON to the MHF binary wire format.
func loadQuestBinary(s *Session, filename string) ([]byte, error) {
base := filepath.Join(s.server.erupeConfig.BinPath, "quests", filename)
if data, err := os.ReadFile(base + ".bin"); err == nil {
return data, nil
}
jsonData, err := os.ReadFile(base + ".json")
if err != nil {
return nil, err
}
compiled, err := CompileQuestJSON(jsonData)
if err != nil {
return nil, fmt.Errorf("compile quest JSON %s: %w", filename, err)
}
return compiled, nil
}
func seasonConversion(s *Session, questFile string) string {
// Try the seasonal override file (e.g., 00001d2 for season 2)
filename := fmt.Sprintf("%s%d", questFile[:6], s.server.Season())