mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-05-06 22:35:11 +02:00
feat(i18n): localized quest text and per-lang quest cache
Phase B of #188. Quest JSON title/description/text_main/text_sub_a/ text_sub_b/success_cond/fail_cond/contractor now accept either a plain string (existing behaviour) or a language-keyed object like "title": { "jp": "...", "en": "...", "fr": "..." } CompileQuestJSON takes the compiling session's language and resolves each field through a fallback chain (requested -> plain -> jp -> en -> any non-empty), so existing single-language quest JSONs keep working byte-for-byte unchanged. The quest cache is re-keyed on (questID, language) so compiled binaries for different languages never leak between sessions on a multi-language server. loadQuestBinary and loadQuestFile now pass s.Lang() into both the compiler and the cache. ParseQuestBinary emits plain-string LocalizedStrings, so the binary -> JSON -> binary round-trip still produces identical output. The new LocalizedString type lives in its own file and is reusable by phase C (scenarios, mail templates, shop text). Shift-JIS encoding still applies to the wire format, so localized values must use characters representable in Shift-JIS — ASCII, kana, CJK — which is documented on the type.
This commit is contained in:
@@ -5,11 +5,22 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// QuestCache is a thread-safe, expiring cache for parsed quest file data.
|
||||
// questCacheKey identifies a cached quest variant. Phase B of #188 added the
|
||||
// language dimension so localized quest text compiled for one session does
|
||||
// not leak into another session's response.
|
||||
type questCacheKey struct {
|
||||
questID int
|
||||
lang string
|
||||
}
|
||||
|
||||
// QuestCache is a thread-safe, expiring cache for parsed quest file data,
|
||||
// keyed by (questID, language). Entries for different languages are stored
|
||||
// independently so a Japanese client and a French client on the same server
|
||||
// never share compiled binaries.
|
||||
type QuestCache struct {
|
||||
mu sync.RWMutex
|
||||
data map[int][]byte
|
||||
expiry map[int]time.Time
|
||||
data map[questCacheKey][]byte
|
||||
expiry map[questCacheKey]time.Time
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
@@ -17,33 +28,36 @@ type QuestCache struct {
|
||||
// A TTL of 0 disables caching (Get always misses).
|
||||
func NewQuestCache(ttlSeconds int) *QuestCache {
|
||||
return &QuestCache{
|
||||
data: make(map[int][]byte),
|
||||
expiry: make(map[int]time.Time),
|
||||
data: make(map[questCacheKey][]byte),
|
||||
expiry: make(map[questCacheKey]time.Time),
|
||||
ttl: time.Duration(ttlSeconds) * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns cached quest data if it exists and has not expired.
|
||||
func (c *QuestCache) Get(questID int) ([]byte, bool) {
|
||||
// Get returns cached quest data for the (questID, lang) variant if it exists
|
||||
// and has not expired.
|
||||
func (c *QuestCache) Get(questID int, lang string) ([]byte, bool) {
|
||||
if c.ttl <= 0 {
|
||||
return nil, false
|
||||
}
|
||||
k := questCacheKey{questID: questID, lang: lang}
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
b, ok := c.data[questID]
|
||||
b, ok := c.data[k]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if time.Now().After(c.expiry[questID]) {
|
||||
if time.Now().After(c.expiry[k]) {
|
||||
return nil, false
|
||||
}
|
||||
return b, true
|
||||
}
|
||||
|
||||
// Put stores quest data in the cache with the configured TTL.
|
||||
func (c *QuestCache) Put(questID int, b []byte) {
|
||||
// Put stores quest data for the (questID, lang) variant with the configured TTL.
|
||||
func (c *QuestCache) Put(questID int, lang string, b []byte) {
|
||||
k := questCacheKey{questID: questID, lang: lang}
|
||||
c.mu.Lock()
|
||||
c.data[questID] = b
|
||||
c.expiry[questID] = time.Now().Add(c.ttl)
|
||||
c.data[k] = b
|
||||
c.expiry[k] = time.Now().Add(c.ttl)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user