From 85cdac036e67569de509d64e2114bb3a0f9a1e8b Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Tue, 17 Feb 2026 01:19:26 +0100 Subject: [PATCH] fix: validate quest file existence in seasonConversion fallbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The final fallback in seasonConversion blindly constructed a filename without checking if it existed on disk. When the file was missing, handleMsgSysGetFile would send doAckBufFail, but the original Frontier client does not gracefully handle this during quest loading — causing a softlock instead of showing the built-in error dialog. Now every fallback path validates file existence before returning, and also tries the opposite time-of-day variant as a last resort. If no file variant exists at all, the original filename is returned with a warning log so the failure ack is still sent. --- server/channelserver/handlers_quest.go | 68 ++++++++++++++++++-------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index b1770be4b..d131a23f8 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -139,31 +139,57 @@ 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))) + return err == 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()) - - // Return the seasonal file - if _, err := os.Stat(filepath.Join(s.server.erupeConfig.BinPath, fmt.Sprintf("quests/%s.bin", filename))); err == nil { + if questFileExists(s, filename) { return filename - } else { - // Attempt to return the requested quest file if the seasonal file doesn't exist - if _, err = os.Stat(filepath.Join(s.server.erupeConfig.BinPath, fmt.Sprintf("quests/%s.bin", questFile))); err == nil { - return questFile - } - - // If the code reaches this point, it's most likely a custom quest with no seasonal variations in the files. - // Since event quests when seasonal pick day or night and the client requests either one, we need to differentiate between the two to prevent issues. - var _time string - - if TimeGameAbsolute() > 2880 { - _time = "d" - } else { - _time = "n" - } - - // Request a d0 or n0 file depending on the time of day. The time of day matters and issues will occur if it's different to the one it requests. - return fmt.Sprintf("%s%s%d", questFile[:5], _time, 0) } + + // Try the originally requested file as-is + if questFileExists(s, questFile) { + return questFile + } + + // Try constructing a day/night base file (e.g., 00001d0 or 00001n0). + // Quest filenames are formatted as [5-digit ID][d/n][season]: e.g., "00001d0". + var currentTime, oppositeTime string + if TimeGameAbsolute() > 2880 { + currentTime = "d" + oppositeTime = "n" + } else { + currentTime = "n" + oppositeTime = "d" + } + + // Try current time-of-day base variant + dayNightFile := fmt.Sprintf("%s%s%d", questFile[:5], currentTime, 0) + if questFileExists(s, dayNightFile) { + return dayNightFile + } + + // Try opposite time-of-day base variant as last resort + oppositeFile := fmt.Sprintf("%s%s%d", questFile[:5], oppositeTime, 0) + if questFileExists(s, oppositeFile) { + s.logger.Warn("Quest file not found for current time, using opposite variant", + zap.String("requested", questFile), + zap.String("using", oppositeFile), + ) + return oppositeFile + } + + // No valid file found. Return the original request so handleMsgSysGetFile + // sends doAckBufFail, which triggers the client's error dialog + // (snj_questd_matching_fail → SetDialogData) instead of a softlock. + s.logger.Warn("No quest file variant found for any season or time-of-day", + zap.String("requested", questFile), + ) + return questFile } func handleMsgMhfLoadFavoriteQuest(s *Session, p mhfpacket.MHFPacket) {