mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-22 21:46:51 +02:00
Merge pull request #1523 from bryanthaboi/dev
some bugs and some switch stuff
This commit is contained in:
@@ -424,11 +424,12 @@ local function renderEffect(data, header, options)
|
||||
return love.audio.newSource(sd, "static")
|
||||
end
|
||||
|
||||
function ChipAudio.newSfx(data, name, pitch, tempo, header)
|
||||
function ChipAudio.newSfx(data, name, pitch, tempo, header, plainFrames)
|
||||
header = header or data.audio.sfx[name]
|
||||
return renderEffect(data, header, {
|
||||
frequencyOffset = pitch or 0,
|
||||
frameTicks = 0x80 + (tempo or 0x80),
|
||||
plainFrames = plainFrames,
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
+12
-6
@@ -280,6 +280,7 @@ function Channel.new(engine, spec, options)
|
||||
allowLoops = options.allowLoops ~= false,
|
||||
frequencyOffset = options.frequencyOffset or 0,
|
||||
frameTicks = options.frameTicks or FRAME_TICKS,
|
||||
plainTicks = (options.plainFrames or 0) * FRAME_TICKS,
|
||||
speed = 12,
|
||||
noteLength = 1, -- Gen 2 CHANNEL_NOTE_LENGTH (note_type)
|
||||
durationModifier = 0, -- Gen 2 fractional-frame carry
|
||||
@@ -349,8 +350,9 @@ function Channel:frequencyGen2(note, octave)
|
||||
return bit.band(register + self.frequencyOffset, 0x7FF)
|
||||
end
|
||||
|
||||
function Channel:durationTicks(length)
|
||||
local tempo = self.sfx and self.frameTicks or self.engine.tempo
|
||||
function Channel:durationTicks(length, plain)
|
||||
local tempo = self.sfx and (plain and FRAME_TICKS or self.frameTicks)
|
||||
or self.engine.tempo
|
||||
local speed = self.sfx and (self.executeMusic and self.speed or 1)
|
||||
or self.speed
|
||||
return length * speed * tempo
|
||||
@@ -586,6 +588,9 @@ function Channel:nextEvent()
|
||||
local packed = self:byte()
|
||||
local volume = bit.rshift(packed, 4)
|
||||
local fade = fadeValue(bit.band(packed, 0x0F))
|
||||
-- audio/engine_2.asm:991-1013, :1015-1033, :1077-1096
|
||||
local plain = self.timeTicks < self.plainTicks
|
||||
local offset = plain and 0 or self.frequencyOffset
|
||||
if self.noise then
|
||||
-- Audio2_ApplyWavePatternAndFrequency adds wFrequencyModifier to the
|
||||
-- frequency low byte for every channel at or past CHAN5, the noise
|
||||
@@ -595,12 +600,12 @@ function Channel:nextEvent()
|
||||
-- byte that noise does not use for frequency. Dropping it left the
|
||||
-- battle hit sounds at their unmodified pitches, where super effective
|
||||
-- reads as the duller of the two (#826).
|
||||
local parameter = bit.band(self:byte() + self.frequencyOffset, 0xFF)
|
||||
local parameter = bit.band(self:byte() + offset, 0xFF)
|
||||
return self:noiseEvent(
|
||||
self:durationTicks(length), volume, fade, parameter)
|
||||
self:durationTicks(length, plain), volume, fade, parameter)
|
||||
end
|
||||
local register = bit.band(self:word() + self.frequencyOffset, 0x7FF)
|
||||
return self:tone(self:durationTicks(length), register, volume, fade)
|
||||
local register = bit.band(self:word() + offset, 0x7FF)
|
||||
return self:tone(self:durationTicks(length, plain), register, volume, fade)
|
||||
elseif command == 0x10 then
|
||||
local packed = self:byte()
|
||||
self.sweep = {
|
||||
@@ -1260,6 +1265,7 @@ function Engine.new(data, header, options)
|
||||
allowLoops = options.allowLoops,
|
||||
frequencyOffset = options.frequencyOffset,
|
||||
frameTicks = frameTicks,
|
||||
plainFrames = options.plainFrames,
|
||||
})
|
||||
end
|
||||
return engine
|
||||
|
||||
+5
-5
@@ -120,11 +120,11 @@ local function visibleBaseState(stack)
|
||||
end
|
||||
|
||||
local function loadGenerated(path)
|
||||
local chunk = love.filesystem.load(path)
|
||||
if not chunk then return nil end
|
||||
local ok, data = pcall(chunk)
|
||||
if ok then return data end
|
||||
return nil
|
||||
-- CacheFs.loadActive, not love.filesystem.load: Gold's cache lives under
|
||||
-- gold/ and fused NX often cannot mount that tree onto data/generated/.
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
local data = CacheFs.loadActive(path)
|
||||
return data
|
||||
end
|
||||
|
||||
-- NewGame (engine/menus/intro_menu.asm) calls OakSpeech, and OakSpeech's first
|
||||
|
||||
+30
-13
@@ -1,10 +1,11 @@
|
||||
-- NX-only asset overlay: fused love-nx cannot reliably mount
|
||||
-- blue|yellow/assets/generated onto the un-prefixed assets/generated, so
|
||||
-- blue|yellow|gold/{assets,data}/generated onto the un-prefixed paths, so
|
||||
-- instead of teaching every call site about versioned caches, this module
|
||||
-- wraps EVERY read-side love entry point that accepts a filesystem path
|
||||
-- once at boot: any string path under assets/generated/ that does not
|
||||
-- resolve falls back to the active version's prefixed copy
|
||||
-- (yellow|blue/assets/generated/...). Covering the whole read surface --
|
||||
-- once at boot: any string path under assets/generated/ or data/generated/
|
||||
-- prefers the active version's prefixed copy (yellow|blue|gold/...) when
|
||||
-- that file exists, so leftover unprefixed Red cache cannot shadow it.
|
||||
-- Covering the whole read surface --
|
||||
-- not just the loaders we happened to need -- is what keeps future states
|
||||
-- and mods inside the fallback without anyone updating this file.
|
||||
--
|
||||
@@ -18,27 +19,43 @@
|
||||
-- * the chip-audio worker (src/core/chip_worker.lua) is a separate Lua
|
||||
-- state without these wrappers; ChipAudio.slimAudio hands it the prefix
|
||||
-- explicitly as audio.programPrefix.
|
||||
-- * data/generated module loads go through CacheFs.readActive, which
|
||||
-- already implements the same fallback for require bytes.
|
||||
-- * Gen 1 Data:load and Gold Game2/World go through CacheFs.readActive /
|
||||
-- CacheFs.loadActive, which implement the same fallback for Lua bytes.
|
||||
-- data/generated is still rewritten here so any leftover
|
||||
-- love.filesystem.load("data/generated/...") call (the Gold intro /
|
||||
-- naming / maps hole on 0.2.4) stays inside the overlay.
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
|
||||
local GENERATED = "assets/generated/"
|
||||
local GENERATED_PREFIXES = {
|
||||
"assets/generated/",
|
||||
"data/generated/",
|
||||
}
|
||||
|
||||
local NxAssetOverlay = {}
|
||||
|
||||
local originals -- raw love functions, non-nil while installed
|
||||
|
||||
-- Resolve `path` to the versioned copy when the un-prefixed file is missing
|
||||
-- and the active version (Blue/Yellow) carries it. Returns nil when the
|
||||
-- caller's path should be used untouched (non-generated path, Red, the real
|
||||
-- file exists, or no versioned copy).
|
||||
-- Resolve `path` to the active version's prefixed copy when that file
|
||||
-- exists (gold|yellow|blue|red/{assets,data}/generated/...). The versioned
|
||||
-- tree wins over a leftover un-prefixed file so a pre-#899 Red root cache
|
||||
-- (assets/generated/font.png in the save dir) cannot shadow Gold/Blue/
|
||||
-- Yellow art that shares a name. Returns nil when the caller's path
|
||||
-- should be used untouched (non-generated path, empty prefix, or no
|
||||
-- versioned copy).
|
||||
local function versioned(path)
|
||||
if type(path) ~= "string" then return nil end
|
||||
if path:sub(1, #GENERATED) ~= GENERATED then return nil end
|
||||
local generated = false
|
||||
for i = 1, #GENERATED_PREFIXES do
|
||||
local gen = GENERATED_PREFIXES[i]
|
||||
if path:sub(1, #gen) == gen then
|
||||
generated = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not generated then return nil end
|
||||
local prefix = GameVersion.cachePrefix()
|
||||
if prefix == "" then return nil end
|
||||
if originals.getInfo(path) then return nil end
|
||||
local candidate = prefix .. path
|
||||
if originals.getInfo(candidate) then return candidate end
|
||||
return nil
|
||||
|
||||
+109
-36
@@ -8,6 +8,7 @@
|
||||
local Assets = require("src.render.Assets")
|
||||
local Logger = require("src.core.Logger")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local bit = require("bit")
|
||||
|
||||
local Sound = {}
|
||||
|
||||
@@ -142,10 +143,10 @@ local function newFileSource(def)
|
||||
return s
|
||||
end
|
||||
|
||||
local function newSfxSource(data, key, def, pitch, tempo)
|
||||
local function newSfxSource(data, key, def, pitch, tempo, plain)
|
||||
if isChipDef(def) then
|
||||
local ok, s = pcall(require("src.core.ChipAudio").newSfx,
|
||||
data, key:match("^([^@]+)") or key, pitch, tempo, def)
|
||||
data, key:match("^([^@]+)") or key, pitch, tempo, def, plain)
|
||||
if not ok then return nil, tostring(s) end
|
||||
if not s then return nil, "no source" end
|
||||
return s
|
||||
@@ -153,12 +154,12 @@ local function newSfxSource(data, key, def, pitch, tempo)
|
||||
return newFileSource(def)
|
||||
end
|
||||
|
||||
local function playPath(data, key, def, pitch, tempo)
|
||||
local function playPath(data, key, def, pitch, tempo, plain)
|
||||
if not love.audio or not def then return nil end
|
||||
local src = cache[key]
|
||||
if src == false then return nil end -- known bad, already logged
|
||||
if not src then
|
||||
local s, err = newSfxSource(data, key, def, pitch, tempo)
|
||||
local s, err = newSfxSource(data, key, def, pitch, tempo, plain)
|
||||
if not s then
|
||||
cache[key] = false
|
||||
reportBadDef("sfx", key, owner(data, "sfx", key), err)
|
||||
@@ -413,50 +414,109 @@ end
|
||||
-- still sounding when the second row starts, so the original never plays
|
||||
-- SFX_BATTLE_2A (CHAN5+6+8) at all -- unguarded, its tail is heard running
|
||||
-- past the end of the animation (#844).
|
||||
local lastMoveSfx -- { src, rank, engine, channels } of the last row sound
|
||||
local moveSfxChannels = {} -- software channel (5-8) -> { src, address, engine }
|
||||
|
||||
local function channelsOverlap(a, b)
|
||||
if not (a and b) then return false end
|
||||
for _, x in ipairs(a) do
|
||||
for _, y in ipairs(b) do
|
||||
if x == y then return true end
|
||||
end
|
||||
local function sourceAlive(entry)
|
||||
local ok, playing = pcall(entry.src.isPlaying, entry.src)
|
||||
return ok and playing
|
||||
end
|
||||
|
||||
local function pruneMoveSfx()
|
||||
for ch, cur in pairs(moveSfxChannels) do
|
||||
if not sourceAlive(cur) then moveSfxChannels[ch] = nil end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- would PlaySound start this def now? Taking a channel over also stops the
|
||||
-- sound that held it, the way .playChannel resets the channel.
|
||||
local function sfxChannelGate(data, def)
|
||||
local cur = lastMoveSfx
|
||||
if not cur then return true end
|
||||
local ok, playing = pcall(cur.src.isPlaying, cur.src)
|
||||
if not (ok and playing) then
|
||||
lastMoveSfx = nil
|
||||
return true
|
||||
end
|
||||
pruneMoveSfx()
|
||||
-- an unrankable def (file asset, or another engine's bank) has no
|
||||
-- comparable sound id: leave it to the mixer, as before
|
||||
if type(def) ~= "table" or not def.address or def.engine ~= cur.engine then
|
||||
return true
|
||||
end
|
||||
if type(def) ~= "table" or not def.address then return true end
|
||||
local channels = require("src.core.ChipSynth").effectChannels(data, def)
|
||||
if not channelsOverlap(channels, cur.channels) then return true end
|
||||
if def.address > cur.rank then return false end
|
||||
pcall(cur.src.stop, cur.src)
|
||||
lastMoveSfx = nil
|
||||
return true
|
||||
if not channels then return true end
|
||||
local takeover
|
||||
for _, ch in ipairs(channels) do
|
||||
local cur = moveSfxChannels[ch]
|
||||
if cur and cur.engine == def.engine then
|
||||
if def.address > cur.address then return false end
|
||||
takeover = takeover or {}
|
||||
takeover[cur.src] = true
|
||||
end
|
||||
end
|
||||
if takeover then
|
||||
for ch, cur in pairs(moveSfxChannels) do
|
||||
if takeover[cur.src] then moveSfxChannels[ch] = nil end
|
||||
end
|
||||
end
|
||||
return true, takeover
|
||||
end
|
||||
|
||||
local function noteMoveSfx(data, def, src)
|
||||
if not src or type(def) ~= "table" or not def.address then
|
||||
lastMoveSfx = nil
|
||||
moveSfxChannels = {}
|
||||
return
|
||||
end
|
||||
lastMoveSfx = {
|
||||
src = src, rank = def.address, engine = def.engine,
|
||||
channels = require("src.core.ChipSynth").effectChannels(data, def),
|
||||
}
|
||||
local channels = require("src.core.ChipSynth").effectChannels(data, def)
|
||||
if not channels then
|
||||
moveSfxChannels = {}
|
||||
return
|
||||
end
|
||||
local entry = { src = src, address = def.address, engine = def.engine }
|
||||
for _, ch in ipairs(channels) do moveSfxChannels[ch] = entry end
|
||||
end
|
||||
|
||||
-- constants/music_constants.asm:4
|
||||
local function sfxHeaderId(def)
|
||||
if type(def) ~= "table" or not def.address then return nil end
|
||||
local rel = def.address - 0x4000
|
||||
if rel <= 0 or rel % 3 ~= 0 then return nil end
|
||||
return rel / 3
|
||||
end
|
||||
|
||||
local function remainingFrames(src)
|
||||
local ok, dur = pcall(src.getDuration, src, "seconds")
|
||||
if not ok or type(dur) ~= "number" then return nil end
|
||||
local pos
|
||||
ok, pos = pcall(src.tell, src, "seconds")
|
||||
if not ok or type(pos) ~= "number" then return nil end
|
||||
return math.max(0, math.ceil((dur - pos) * 60))
|
||||
end
|
||||
|
||||
-- audio/engine_2.asm:1077-1096, :991-1013, :1015-1033
|
||||
local function plainMoveFrames(data, def, channels)
|
||||
local id = sfxHeaderId(def)
|
||||
local sfx = data.audio and data.audio.sfx
|
||||
if not (id and sfx and channels) then return 0 end
|
||||
local first = sfxHeaderId(sfx.Peck) -- constants/music_constants.asm:178
|
||||
local last = sfxHeaderId(sfx.Trainer_Appeared) -- constants/music_constants.asm:228
|
||||
if not (first and last) then return 0 end
|
||||
local claimed = {}
|
||||
for _, ch in ipairs(channels) do claimed[ch] = true end
|
||||
local base = (claimed[5] or claimed[8]) and id or 0
|
||||
local others = {}
|
||||
for _, ch in ipairs({ 5, 8 }) do
|
||||
local cur = (not claimed[ch]) and moveSfxChannels[ch] or nil
|
||||
if cur and cur.engine == def.engine and sourceAlive(cur) then
|
||||
local otherId = sfxHeaderId(cur)
|
||||
local rem = remainingFrames(cur.src)
|
||||
if otherId and rem and rem > 0 then
|
||||
others[#others + 1] = { id = otherId, rem = rem }
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(others, function(a, b) return a.rem < b.rem end)
|
||||
local plain = 0
|
||||
for drop = 0, #others do
|
||||
local combined = base
|
||||
for i = drop + 1, #others do
|
||||
combined = bit.bor(combined, others[i].id)
|
||||
end
|
||||
if combined >= first and combined <= last then return plain end
|
||||
if drop < #others then plain = others[drop + 1].rem end
|
||||
end
|
||||
return plain
|
||||
end
|
||||
|
||||
function Sound.playMove(data, anim)
|
||||
@@ -466,13 +526,20 @@ function Sound.playMove(data, anim)
|
||||
local name = anim.sound
|
||||
local pitch, tempo = anim.pitch or 0, anim.tempo or 0x80
|
||||
local def = sfx[name]
|
||||
if not sfxChannelGate(data, def) then return end
|
||||
local allowed, superseded = sfxChannelGate(data, def)
|
||||
if not allowed then return end
|
||||
local src
|
||||
-- a chip program synthesizes the modified variant on demand; a file def
|
||||
-- can only reach for a pre-rendered one
|
||||
if isChipDef(def) then
|
||||
src = playPath(data, ("%s@%02x%02x"):format(name, pitch, tempo),
|
||||
def, pitch, tempo)
|
||||
local plain = 0
|
||||
if pitch ~= 0 or tempo ~= 0x80 then
|
||||
local channels = require("src.core.ChipSynth").effectChannels(data, def)
|
||||
plain = plainMoveFrames(data, def, channels)
|
||||
end
|
||||
local key = ("%s@%02x%02x"):format(name, pitch, tempo)
|
||||
if plain > 0 then key = ("%s~%d"):format(key, plain) end
|
||||
src = playPath(data, key, def, pitch, tempo, plain)
|
||||
else
|
||||
local key = ("%s@%02x%02x"):format(name, pitch, tempo)
|
||||
if (pitch ~= 0 or tempo ~= 0x80) and sfx[key] then
|
||||
@@ -481,6 +548,12 @@ function Sound.playMove(data, anim)
|
||||
src = playPath(data, name, def)
|
||||
end
|
||||
end
|
||||
-- audio/engine_2.asm:1537
|
||||
if superseded then
|
||||
for old in pairs(superseded) do
|
||||
if old ~= src then pcall(old.stop, old) end
|
||||
end
|
||||
end
|
||||
if src then
|
||||
played("move", name)
|
||||
noteMoveSfx(data, def, src)
|
||||
@@ -711,7 +784,7 @@ end
|
||||
-- hot reload / jukebox A-B: drop one key's sources (its pitch-tempo
|
||||
-- variants included) or all of them, so the next play re-resolves the def
|
||||
function Sound.invalidate(name)
|
||||
lastMoveSfx = nil -- its source is about to be dropped or stopped
|
||||
moveSfxChannels = {} -- their sources are about to be dropped or stopped
|
||||
-- Same for wCurSFX, and a reloaded table can repoint the id order.
|
||||
curSfx = nil
|
||||
sfxIds = nil
|
||||
|
||||
+90
-4
@@ -142,7 +142,11 @@ function TouchSkin.parse(text)
|
||||
controls = {},
|
||||
}
|
||||
if page.imagePath == "" then page.imagePath = nil end
|
||||
if not page.aspect or page.aspect <= 0 then
|
||||
-- An explicit aspect_ratio is the overlay's design aspect. RetroArch
|
||||
-- letterboxes to it even when full_screen is set, so range_x/range_y
|
||||
-- that were authored as a circle stay a circle. #1503
|
||||
page.aspectFromCfg = page.aspect ~= nil and page.aspect > 0
|
||||
if not page.aspectFromCfg then
|
||||
page.aspect = page.name:lower():find("portrait", 1, true)
|
||||
and PORTRAIT_ASPECT or DEFAULT_ASPECT
|
||||
end
|
||||
@@ -170,6 +174,12 @@ function TouchSkin.parse(text)
|
||||
pages[#pages + 1] = page
|
||||
end
|
||||
|
||||
-- RetroArch auto-rotate: overlay names containing portrait / landscape
|
||||
-- are the lock. Stamp it so the studio does not need a second click. #1503
|
||||
for _, page in ipairs(pages) do
|
||||
if not page.orient then page.orient = TouchSkin.pageOrient(page) end
|
||||
end
|
||||
|
||||
return { pages = pages }
|
||||
end
|
||||
|
||||
@@ -255,6 +265,9 @@ function TouchSkin.parseNative(text)
|
||||
rangeMod = num(raw.rangeMod, 1),
|
||||
alphaMod = num(raw.alphaMod, 1),
|
||||
aspect = num(raw.aspect, DEFAULT_ASPECT),
|
||||
aspectFromCfg = raw.fitAspect == true,
|
||||
orient = (raw.orient == "portrait" or raw.orient == "landscape"
|
||||
or raw.orient == "any") and raw.orient or nil,
|
||||
rect = { x = 0, y = 0, w = 1, h = 1 },
|
||||
controls = {},
|
||||
}
|
||||
@@ -287,6 +300,7 @@ function TouchSkin.parseNative(text)
|
||||
nextTarget = c.nextTarget,
|
||||
}
|
||||
end
|
||||
if not page.orient then page.orient = TouchSkin.pageOrient(page) end
|
||||
pages[#pages + 1] = page
|
||||
end
|
||||
return { pages = pages, name = data.name, author = data.author,
|
||||
@@ -309,6 +323,9 @@ function TouchSkin.toNative(skin)
|
||||
rangeMod = page.rangeMod,
|
||||
alphaMod = page.alphaMod,
|
||||
aspect = page.aspect,
|
||||
fitAspect = page.aspectFromCfg or nil,
|
||||
orient = (page.orient == "portrait" or page.orient == "landscape"
|
||||
or page.orient == "any") and page.orient or nil,
|
||||
controls = {},
|
||||
}
|
||||
if page.rect and (page.rect.x ~= 0 or page.rect.y ~= 0
|
||||
@@ -704,6 +721,10 @@ end
|
||||
|
||||
TouchSkin.active = nil
|
||||
TouchSkin.pageIndex = 1
|
||||
-- RetroArch Auto-Rotate Overlay (1.7.9, default on mobile): a cfg whose
|
||||
-- pages are named portrait / landscape is swapped to match the display.
|
||||
-- The Skin Studio turns this off so PAGE and the canvas preset stay independent.
|
||||
TouchSkin.autoOrient = true
|
||||
|
||||
TouchSkin.surfaceRect = nil
|
||||
|
||||
@@ -732,9 +753,68 @@ function TouchSkin.select(id)
|
||||
return TouchSkin.setActive(skin)
|
||||
end
|
||||
|
||||
local function displaySize()
|
||||
local r = TouchSkin.surfaceRect
|
||||
if r and r.w and r.h and r.w > 0 and r.h > 0 then return r.w, r.h end
|
||||
if love and love.graphics and love.graphics.getDimensions then
|
||||
return love.graphics.getDimensions()
|
||||
end
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
-- Explicit lock (studio) wins; otherwise the page name, the RetroArch
|
||||
-- auto-rotate convention. "any" means unlocked even if the name says
|
||||
-- portrait or landscape.
|
||||
function TouchSkin.pageOrient(page)
|
||||
if not page then return nil end
|
||||
if page.orient == "any" then return nil end
|
||||
if page.orient == "portrait" or page.orient == "landscape" then
|
||||
return page.orient
|
||||
end
|
||||
local n = tostring(page.name or ""):lower()
|
||||
if n:find("landscape", 1, true) then return "landscape" end
|
||||
if n:find("portrait", 1, true) then return "portrait" end
|
||||
return nil
|
||||
end
|
||||
|
||||
function TouchSkin.hasOrientPair(skin)
|
||||
local saw = {}
|
||||
for _, page in ipairs(skin and skin.pages or {}) do
|
||||
local o = TouchSkin.pageOrient(page)
|
||||
if o then saw[o] = true end
|
||||
end
|
||||
return saw.portrait == true and saw.landscape == true
|
||||
end
|
||||
|
||||
local function findOrientPage(skin, keyword)
|
||||
for i, page in ipairs(skin.pages or {}) do
|
||||
if TouchSkin.pageOrient(page) == keyword then return i end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- If the current page is the wrong orientation of a portrait/landscape pair,
|
||||
-- jump to the matching one. Pages locked to neither (gb_anim's GameBoy /
|
||||
-- GameBoyColor) are left alone. #1503
|
||||
function TouchSkin.syncOrientation(w, h)
|
||||
if not TouchSkin.autoOrient then return end
|
||||
local skin = TouchSkin.active
|
||||
if not skin or not w or not h or w <= 0 or h <= 0 then return end
|
||||
local want = w > h and "landscape" or "portrait"
|
||||
local unwant = w > h and "portrait" or "landscape"
|
||||
local page = skin.pages[TouchSkin.pageIndex] or skin.pages[1]
|
||||
local current = TouchSkin.pageOrient(page)
|
||||
if current == want then return end
|
||||
if current ~= unwant then return end
|
||||
local idx = findOrientPage(skin, want)
|
||||
if idx then TouchSkin.pageIndex = idx end
|
||||
end
|
||||
|
||||
function TouchSkin.page()
|
||||
local skin = TouchSkin.active
|
||||
if not skin then return nil end
|
||||
local w, h = displaySize()
|
||||
TouchSkin.syncOrientation(w, h)
|
||||
return skin.pages[TouchSkin.pageIndex] or skin.pages[1]
|
||||
end
|
||||
|
||||
@@ -766,7 +846,12 @@ function TouchSkin.pageBox(page, w, h, ox, oy)
|
||||
ox, oy = ox or 0, oy or 0
|
||||
if not page then return ox, oy, w, h end
|
||||
local bx, by, bw, bh = ox, oy, w, h
|
||||
if not page.fullScreen and h > 0 then
|
||||
-- full_screen means "relative to the window, not the game viewport".
|
||||
-- When the cfg also names an aspect_ratio, that window is then fitted
|
||||
-- to the overlay's design aspect so buttons do not stretch. #1503
|
||||
local fit = ((not page.fullScreen) or page.aspectFromCfg)
|
||||
and page.aspect and page.aspect > 0 and h > 0
|
||||
if fit then
|
||||
local displayAspect = w / h
|
||||
if displayAspect > page.aspect then
|
||||
bw = h * page.aspect
|
||||
@@ -833,8 +918,9 @@ function TouchSkin.viewport(w, h, ox, oy)
|
||||
local page = TouchSkin.page()
|
||||
if not page or not page.viewport or not TouchSkin.drawable() then return nil end
|
||||
local v = page.viewport
|
||||
local x, y = (ox or 0) + v.x * w, (oy or 0) + v.y * h
|
||||
local vw, vh = v.w * w, v.h * h
|
||||
local bx, by, bw, bh = TouchSkin.pageBox(page, w, h, ox, oy)
|
||||
local x, y = bx + v.x * bw, by + v.y * bh
|
||||
local vw, vh = v.w * bw, v.h * bh
|
||||
if vw <= 0 or vh <= 0 then return nil end
|
||||
return x, y, vw, vh, page.viewportFill == true, page.viewportExpand == true
|
||||
end
|
||||
|
||||
@@ -220,6 +220,9 @@ function SwitchDiagnostics.probeAssets(version)
|
||||
"assets/generated/tilesets/reds_house.png",
|
||||
"assets/generated/sprites/red.png",
|
||||
"assets/generated/sprites/monster.png",
|
||||
"data/generated/maps.lua",
|
||||
"data/generated/oak_speech.lua",
|
||||
"data/generated/font.lua",
|
||||
}
|
||||
for _, path in ipairs(samples) do
|
||||
local versioned = prefix ~= "" and (prefix .. path) or path
|
||||
@@ -230,17 +233,26 @@ function SwitchDiagnostics.probeAssets(version)
|
||||
lines[#lines + 1] = "versioned=" .. probeInfo(filesystem, versioned)
|
||||
end
|
||||
lines[#lines + 1] = "resolve=" .. tostring(resolved)
|
||||
lines[#lines + 1] = "newImage=" .. probeOpen("image", resolved)
|
||||
lines[#lines + 1] = "newImageData=" .. probeOpen("imageData", resolved)
|
||||
if prefix ~= "" and resolved ~= versioned then
|
||||
lines[#lines + 1] = "newImage_versioned=" .. probeOpen("image", versioned)
|
||||
lines[#lines + 1] = "newImageData_versioned=" .. probeOpen("imageData", versioned)
|
||||
if path:sub(-4) == ".lua" then
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
local loaded, err = CacheFs.loadActive(path)
|
||||
lines[#lines + 1] = "loadActive=" .. (loaded ~= nil and "ok"
|
||||
or ("FAIL " .. tostring(err):gsub("%s+", " "):sub(1, 160)))
|
||||
else
|
||||
lines[#lines + 1] = "newImage=" .. probeOpen("image", resolved)
|
||||
lines[#lines + 1] = "newImageData=" .. probeOpen("imageData", resolved)
|
||||
if prefix ~= "" and resolved ~= versioned then
|
||||
lines[#lines + 1] = "newImage_versioned=" .. probeOpen("image", versioned)
|
||||
lines[#lines + 1] = "newImageData_versioned=" .. probeOpen("imageData", versioned)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Shallow listing so we can see if the extract tree exists at all.
|
||||
local roots = { "yellow", "blue", "assets", "yellow/assets/generated",
|
||||
"yellow/assets/generated/sprites", "blue/assets/generated/sprites" }
|
||||
local roots = { "yellow", "blue", "gold", "assets", "yellow/assets/generated",
|
||||
"yellow/assets/generated/sprites", "blue/assets/generated/sprites",
|
||||
"gold/assets/generated", "gold/assets/generated/sprites",
|
||||
"gold/data/generated" }
|
||||
for _, dir in ipairs(roots) do
|
||||
local info = filesystem.getInfo(dir)
|
||||
if info and info.type == "directory" and filesystem.getDirectoryItems then
|
||||
|
||||
+28
-2
@@ -34,7 +34,7 @@ local SEP = package.config:sub(1, 1)
|
||||
|
||||
-- Cache-relative paths are prefixed with this before every read/write, so a
|
||||
-- version's import lands under its GameVersion.cachePrefix (red/, blue/,
|
||||
-- yellow/). The launcher sets it per import / per readiness check; it stays
|
||||
-- yellow/, gold/). The launcher sets it per import / per readiness check; it stays
|
||||
-- "" outside those flows. Runtime *reads* (require / newImage) do NOT go
|
||||
-- through here -- CacheFs.mountVersion overlays the active version's subtree
|
||||
-- onto the un-prefixed paths instead.
|
||||
@@ -305,7 +305,7 @@ function CacheFs.read(rel)
|
||||
end
|
||||
|
||||
-- Read cache-relative `rel` for the active GameVersion when PhysFS may hide
|
||||
-- prefixed Blue/Yellow trees (fused NX mount hole). Same order Data:load
|
||||
-- prefixed Blue/Yellow/Gold trees (fused NX mount hole). Same order Data:load
|
||||
-- already used: active version prefix with CacheFs.prefix cleared, then
|
||||
-- `rel` under the caller's CacheFs.prefix. Returns the bytes or nil.
|
||||
function CacheFs.readActive(rel)
|
||||
@@ -322,6 +322,32 @@ function CacheFs.readActive(rel)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Load a generated Lua table the way Data:load does: versioned save-dir
|
||||
-- bytes first (gold/data/generated/maps.lua), then the un-prefixed path.
|
||||
-- Game2/World used love.filesystem.load("data/generated/...") which misses
|
||||
-- on fused NX when the gold/ overlay mount fails -- intro art still loads
|
||||
-- via NxAssetOverlay, but oak_speech.lua / font.lua / maps.lua do not.
|
||||
function CacheFs.loadActive(rel)
|
||||
local bytes = CacheFs.readActive(rel)
|
||||
if type(bytes) == "string" then
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local loader = loadstring or load
|
||||
local chunk, err = loader(bytes, "@" .. GameVersion.cachePrefix() .. rel)
|
||||
if not chunk then return nil, err end
|
||||
local ok, value = pcall(chunk)
|
||||
if not ok then return nil, value end
|
||||
return value
|
||||
end
|
||||
if love and love.filesystem and love.filesystem.load then
|
||||
local chunk, err = love.filesystem.load(rel)
|
||||
if not chunk then return nil, err end
|
||||
local ok, value = pcall(chunk)
|
||||
if not ok then return nil, value end
|
||||
return value
|
||||
end
|
||||
return nil, "Could not open file " .. rel .. ". Does not exist."
|
||||
end
|
||||
|
||||
-- does cache-relative `rel` exist as a file?
|
||||
function CacheFs.exists(rel)
|
||||
rel = withPrefix(rel)
|
||||
|
||||
+26
-28
@@ -192,20 +192,28 @@ local PAL = {
|
||||
chipInkGold = { 58, 44, 0 }, -- #3a2c00 dark "Y" on the gold chip
|
||||
}
|
||||
|
||||
-- Per-version required cache files. Gold replaces the Gen 1 list entirely
|
||||
-- (VERSION_REQUIRED_FILES_OVERRIDE); Yellow adds a few extra markers.
|
||||
local function requiredFilesFor(version)
|
||||
local override = VERSION_REQUIRED_FILES_OVERRIDE[version]
|
||||
if override then return override, true end
|
||||
return REQUIRED_FILES, false
|
||||
end
|
||||
|
||||
-- CacheFs.exists checks the game folder directly for a portable install,
|
||||
-- otherwise the save directory through love.filesystem. It honors
|
||||
-- CacheFs.prefix, so we point it at the version's cache subtree (red/,
|
||||
-- blue/, yellow/).
|
||||
-- blue/, yellow/, gold/).
|
||||
local function allRequiredFilesExist(version)
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
local saved = CacheFs.prefix
|
||||
CacheFs.prefix = GameVersion.cachePrefix(version)
|
||||
local ok = true
|
||||
local required = VERSION_REQUIRED_FILES_OVERRIDE[version] or REQUIRED_FILES
|
||||
local required, isOverride = requiredFilesFor(version)
|
||||
for _, path in ipairs(required) do
|
||||
if not CacheFs.exists(path) then ok = false; break end
|
||||
end
|
||||
if ok and not VERSION_REQUIRED_FILES_OVERRIDE[version] then
|
||||
if ok and not isOverride then
|
||||
for _, path in ipairs(VERSION_REQUIRED_FILES[version] or {}) do
|
||||
if not CacheFs.exists(path) then ok = false; break end
|
||||
end
|
||||
@@ -215,20 +223,23 @@ local function allRequiredFilesExist(version)
|
||||
end
|
||||
|
||||
-- A developer checkout / Python build leaves generated data in the physfs
|
||||
-- source: Red at the historical root, Blue/Yellow in their versioned trees.
|
||||
-- Imported Red caches still live under red/. Check source paths directly so
|
||||
-- that cache prefix cannot hide Red's source tree, and keep save-dir caches
|
||||
-- from counting as current source data.
|
||||
-- source: Red at the historical root, Blue/Yellow/Gold in their versioned
|
||||
-- trees. Imported Red caches still live under red/. Check source paths
|
||||
-- directly so that cache prefix cannot hide Red's source tree, and keep
|
||||
-- save-dir caches from counting as current source data.
|
||||
local function sourceTreeHasData(version)
|
||||
if not love.filesystem.getRealDirectory then return false end
|
||||
local prefix = version == "red" and "" or GameVersion.cachePrefix(version)
|
||||
for _, path in ipairs(REQUIRED_FILES) do
|
||||
local required, isOverride = requiredFilesFor(version)
|
||||
for _, path in ipairs(required) do
|
||||
if love.filesystem.getInfo(prefix .. path, "file") == nil then return false end
|
||||
end
|
||||
for _, path in ipairs(VERSION_REQUIRED_FILES[version] or {}) do
|
||||
if love.filesystem.getInfo(prefix .. path, "file") == nil then return false end
|
||||
if not isOverride then
|
||||
for _, path in ipairs(VERSION_REQUIRED_FILES[version] or {}) do
|
||||
if love.filesystem.getInfo(prefix .. path, "file") == nil then return false end
|
||||
end
|
||||
end
|
||||
local path = prefix .. REQUIRED_FILES[1]
|
||||
local path = prefix .. required[1]
|
||||
local real = love.filesystem.getRealDirectory(path)
|
||||
return real == love.filesystem.getSource()
|
||||
end
|
||||
@@ -285,7 +296,7 @@ local function purgeSaveDirCache()
|
||||
return true
|
||||
end
|
||||
-- Purge each version's stale save-directory copy (under its red/ / blue/
|
||||
-- / yellow/ prefix) so it cannot shadow the portable game-folder cache.
|
||||
-- / yellow/ / gold/ prefix) so it cannot shadow the portable game-folder cache.
|
||||
for _, version in ipairs(GameVersion.ORDER) do
|
||||
local prefix = GameVersion.cachePrefix(version)
|
||||
if saveDirHas(prefix .. MARKER_PATH) or saveDirHas(prefix .. REQUIRED_FILES[1]) then
|
||||
@@ -4324,25 +4335,12 @@ function RomImporter:_findRows()
|
||||
category = self.findCategory,
|
||||
})
|
||||
if self.modScope then
|
||||
local ModTargets = require("src.mods.ModTargets")
|
||||
local gen = GameVersion.generation(self.modScope)
|
||||
local kept = {}
|
||||
for _, entry in ipairs(rows) do
|
||||
local has1, has2 = false, false
|
||||
local function note(s)
|
||||
s = tostring(s or ""):lower()
|
||||
if s == "gen1" or s == "gen 1" or s == "red" or s == "blue"
|
||||
or s == "yellow" then
|
||||
has1 = true
|
||||
end
|
||||
if s == "gen2" or s == "gen 2" or s == "gold" then
|
||||
has2 = true
|
||||
end
|
||||
end
|
||||
for _, cat in ipairs(entry.categories or {}) do note(cat) end
|
||||
for _, tag in ipairs(entry.tags or {}) do note(tag) end
|
||||
if (not has1 and not has2)
|
||||
or (gen == 2 and has2)
|
||||
or (gen ~= 2 and has1) then
|
||||
local versions = ModTargets.normalize(entry.games)
|
||||
if #versions == 0 or ModTargets.covers(versions, gen) then
|
||||
kept[#kept + 1] = entry
|
||||
end
|
||||
end
|
||||
|
||||
@@ -141,7 +141,7 @@ function SaveFileIO.exportActiveSlot(version)
|
||||
fs.createDirectory("exports")
|
||||
fs.createDirectory("exports/" .. version)
|
||||
end
|
||||
-- Per-game folder so MTP browsing matches inbox layout (red/blue/yellow).
|
||||
-- Per-game folder so MTP browsing matches inbox layout (red/blue/yellow/gold).
|
||||
local rel = ("exports/%s/gen1recomp-%s-%s.sav"):format(version, version, slotId)
|
||||
local ok, writeErr = fs.write(rel, bytes)
|
||||
if not ok then return false, "could not write the export: " .. tostring(writeErr) end
|
||||
|
||||
@@ -160,6 +160,7 @@ local function parseEntry(raw)
|
||||
summary = str(raw.summary) or "",
|
||||
categories = strArray(raw.categories),
|
||||
tags = strArray(raw.tags),
|
||||
games = strArray(raw.games),
|
||||
license = str(raw.license),
|
||||
repo = str(raw.repo),
|
||||
github = str(raw.github),
|
||||
|
||||
@@ -166,7 +166,8 @@ function EvolutionState:draw()
|
||||
if sprite then
|
||||
local x = math.floor((160 - sprite:getWidth()) / 2)
|
||||
local y = math.max(8, 64 - sprite:getHeight())
|
||||
love.graphics.draw(sprite, x, y)
|
||||
-- engine/movie/evolution.asm:103
|
||||
love.graphics.draw(sprite, x + sprite:getWidth(), y, 0, -1, 1)
|
||||
if spriteTrueColor then
|
||||
require("src.render.PaletteFX").markTrueColor(x, y, sprite:getDimensions())
|
||||
end
|
||||
|
||||
+127
-9
@@ -20,6 +20,17 @@ Studio.CANVASES = {
|
||||
lockViewport = { x = 48 / 256, y = 40 / 224, w = 160 / 256, h = 144 / 224 } },
|
||||
}
|
||||
|
||||
-- Per-page lock, and whether the canvas preset follows it. Match is on
|
||||
-- by default so a portrait/landscape overlay pair does not need two
|
||||
-- separate clicks to preview the right way up. #1503
|
||||
Studio.matchOrient = true
|
||||
Studio.ORIENT_CYCLE = { "any", "portrait", "landscape" }
|
||||
Studio.ORIENT_LABEL = {
|
||||
any = "Lock: Off",
|
||||
portrait = "Lock: Portrait",
|
||||
landscape = "Lock: Landscape",
|
||||
}
|
||||
|
||||
local HANDLE = 7
|
||||
local HANDLES = {
|
||||
{ "nw", 0, 0 }, { "n", 0.5, 0 }, { "ne", 1, 0 },
|
||||
@@ -64,9 +75,91 @@ local function syncActive()
|
||||
TouchSkin.pageIndex = Studio.pageIndex
|
||||
end
|
||||
|
||||
function Studio.setCanvas(index)
|
||||
local function canvasOrientation(canvas)
|
||||
canvas = canvas or Studio.canvas()
|
||||
if not canvas then return nil end
|
||||
return canvas.w > canvas.h and "landscape" or "portrait"
|
||||
end
|
||||
|
||||
local function pickCanvasIndex(want)
|
||||
local cur = Studio.canvas()
|
||||
if canvasOrientation(cur) == want then return Studio.canvasIndex end
|
||||
if cur and cur.id then
|
||||
local hint = cur.id:gsub("portrait", want):gsub("landscape", want)
|
||||
for i, c in ipairs(Studio.CANVASES) do
|
||||
if c.id == hint then return i end
|
||||
end
|
||||
end
|
||||
for i, c in ipairs(Studio.CANVASES) do
|
||||
if canvasOrientation(c) == want and not c.lockViewport then return i end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function Studio.applyImportedOrient()
|
||||
if not Studio.skin then return false end
|
||||
if not TouchSkin.hasOrientPair(Studio.skin) then
|
||||
Studio.syncCanvasToPage()
|
||||
return false
|
||||
end
|
||||
-- A RetroArch overlay that already auto-rotates should do the same in
|
||||
-- the studio: lock is on, canvas follows, and the visible page matches
|
||||
-- the mock device. #1503
|
||||
Studio.matchOrient = true
|
||||
Studio.syncPageToCanvas()
|
||||
Studio.syncCanvasToPage()
|
||||
return true
|
||||
end
|
||||
|
||||
function Studio.syncCanvasToPage()
|
||||
if not Studio.matchOrient then return end
|
||||
local want = TouchSkin.pageOrient(Studio.page())
|
||||
if not want then return end
|
||||
local idx = pickCanvasIndex(want)
|
||||
if idx and idx ~= Studio.canvasIndex then Studio.setCanvas(idx, true) end
|
||||
end
|
||||
|
||||
function Studio.syncPageToCanvas()
|
||||
if not Studio.matchOrient or not Studio.skin then return end
|
||||
local want = canvasOrientation()
|
||||
if TouchSkin.pageOrient(Studio.page()) == want then return end
|
||||
for i, page in ipairs(Studio.skin.pages or {}) do
|
||||
if TouchSkin.pageOrient(page) == want then
|
||||
Studio.pageIndex = i
|
||||
Studio.selected = nil
|
||||
syncActive()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Studio.cyclePageOrient(dir)
|
||||
local page = Studio.page()
|
||||
if not page then return end
|
||||
local cur = TouchSkin.pageOrient(page) or "any"
|
||||
local idx = 1
|
||||
for i, o in ipairs(Studio.ORIENT_CYCLE) do
|
||||
if o == cur then idx = i break end
|
||||
end
|
||||
local n = #Studio.ORIENT_CYCLE
|
||||
local nxt = Studio.ORIENT_CYCLE[((idx - 1 + (dir or 1)) % n) + 1]
|
||||
page.orient = nxt
|
||||
local name = tostring(page.name or "")
|
||||
if (nxt == "portrait" or nxt == "landscape")
|
||||
and (name == "" or name == "main" or name:match("^page%d+$")) then
|
||||
page.name = nxt
|
||||
end
|
||||
markDirty()
|
||||
Studio.syncCanvasToPage()
|
||||
return nxt
|
||||
end
|
||||
|
||||
function Studio.setCanvas(index, fromSync)
|
||||
local n = #Studio.CANVASES
|
||||
Studio.canvasIndex = ((index - 1) % n) + 1
|
||||
-- Pick the matching page before writing canvas-owned fields onto it,
|
||||
-- so a landscape preset does not stamp a portrait page. #1503
|
||||
if not fromSync then Studio.syncPageToCanvas() end
|
||||
local canvas = Studio.canvas()
|
||||
local page = Studio.page()
|
||||
if page and canvas.lockViewport then
|
||||
@@ -77,7 +170,11 @@ function Studio.setCanvas(index)
|
||||
page.viewportFill = false
|
||||
markDirty()
|
||||
end
|
||||
if page then page.aspect = canvas.w / canvas.h end
|
||||
-- A cfg-authored aspect_ratio is the overlay's design aspect; keep it so
|
||||
-- the preview letterboxes like RetroArch instead of stretching (#1503).
|
||||
if page and not page.aspectFromCfg then
|
||||
page.aspect = canvas.w / canvas.h
|
||||
end
|
||||
end
|
||||
|
||||
function Studio.load(opts)
|
||||
@@ -102,6 +199,10 @@ function Studio.load(opts)
|
||||
TouchControls.active = true
|
||||
TouchControls.enabled = true
|
||||
TouchControls:setPreview(true)
|
||||
-- Play snaps pages from the window aspect. The studio uses its own
|
||||
-- Match canvas toggle against the mock device instead. #1503
|
||||
TouchSkin.autoOrient = false
|
||||
Studio.matchOrient = true
|
||||
|
||||
local start = opts.skinId
|
||||
if not start then
|
||||
@@ -130,6 +231,7 @@ function Studio.open(id)
|
||||
Studio.dirty = false
|
||||
Studio.images = TouchSkin.listImages(Studio.skin.root)
|
||||
syncActive()
|
||||
Studio.applyImportedOrient()
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -137,6 +239,7 @@ function Studio.unload()
|
||||
Studio.pendingPlay = false
|
||||
TouchSkin.setSurface(nil)
|
||||
TouchSkin.setActive(nil)
|
||||
TouchSkin.autoOrient = true
|
||||
TouchControls:setPreview(false)
|
||||
TouchControls:reset()
|
||||
Studio.skin = nil
|
||||
@@ -358,6 +461,7 @@ function Studio.addPage()
|
||||
Studio.pageIndex = #skin.pages
|
||||
Studio.selected = nil
|
||||
syncActive()
|
||||
Studio.syncCanvasToPage()
|
||||
markDirty()
|
||||
end
|
||||
|
||||
@@ -432,7 +536,8 @@ end
|
||||
local function viewportRect(page, r)
|
||||
local v = page.viewport
|
||||
if not v then return nil end
|
||||
return r.x + v.x * r.w, r.y + v.y * r.h, v.w * r.w, v.h * r.h
|
||||
local bx, by, bw, bh = TouchSkin.pageBox(page, r.w, r.h, r.x, r.y)
|
||||
return bx + v.x * bw, by + v.y * bh, v.w * bw, v.h * bh
|
||||
end
|
||||
|
||||
local function handleRects(bx, by, bw, bh)
|
||||
@@ -540,17 +645,19 @@ function Studio.updateDrag(mx, my, r)
|
||||
end
|
||||
end
|
||||
|
||||
local px, py, pw, ph = TouchSkin.pageBox(page, r.w, r.h, r.x, r.y)
|
||||
if pw <= 0 or ph <= 0 then return end
|
||||
if d.kind:find("control") then
|
||||
local ctl = Studio.selectedControl()
|
||||
if not ctl then return end
|
||||
ctl.x = clamp01(((bx + bw * 0.5) - r.x) / r.w)
|
||||
ctl.y = clamp01(((by + bh * 0.5) - r.y) / r.h)
|
||||
ctl.rangeX = math.max(0.002, (bw * 0.5) / r.w)
|
||||
ctl.rangeY = math.max(0.002, (bh * 0.5) / r.h)
|
||||
ctl.x = clamp01(((bx + bw * 0.5) - px) / pw)
|
||||
ctl.y = clamp01(((by + bh * 0.5) - py) / ph)
|
||||
ctl.rangeX = math.max(0.002, (bw * 0.5) / pw)
|
||||
ctl.rangeY = math.max(0.002, (bh * 0.5) / ph)
|
||||
else
|
||||
page.viewport = {
|
||||
x = clamp01((bx - r.x) / r.w), y = clamp01((by - r.y) / r.h),
|
||||
w = math.max(0.02, bw / r.w), h = math.max(0.02, bh / r.h),
|
||||
x = clamp01((bx - px) / pw), y = clamp01((by - py) / ph),
|
||||
w = math.max(0.02, bw / pw), h = math.max(0.02, bh / ph),
|
||||
}
|
||||
end
|
||||
markDirty()
|
||||
@@ -665,11 +772,22 @@ local function inspectorBody(x, y, w)
|
||||
Studio.pageIndex = (Studio.pageIndex % #Studio.skin.pages) + 1
|
||||
Studio.selected = nil
|
||||
syncActive()
|
||||
Studio.syncCanvasToPage()
|
||||
end
|
||||
if Kit.button(x + half + gap, cy, half, rowH, "Add page", { id = "pageadd" }) then
|
||||
Studio.addPage()
|
||||
end
|
||||
cy = cy + rowH + gap
|
||||
local lock = TouchSkin.pageOrient(page) or "any"
|
||||
if Kit.button(x, cy, half, rowH, Studio.ORIENT_LABEL[lock] or "Lock: Off",
|
||||
{ id = "orient" }) then
|
||||
Studio.cyclePageOrient(1)
|
||||
end
|
||||
local matchOn = Studio.matchOrient
|
||||
Studio.matchOrient = Kit.checkbox(x + half + gap, cy, half, rowH,
|
||||
Studio.matchOrient, "Match canvas", "matchorient")
|
||||
if Studio.matchOrient and not matchOn then Studio.syncCanvasToPage() end
|
||||
cy = cy + rowH + gap
|
||||
|
||||
if page then
|
||||
local bezel = page.imagePath or "(none)"
|
||||
|
||||
@@ -445,7 +445,8 @@ function TradeAnim:draw()
|
||||
-- mon pic on the BG at hlcoord 7, 2; info box on the window at
|
||||
-- hWY $50, so it sits in the bottom half of the screen
|
||||
if self.monVisible and self.sentSprite then
|
||||
love.graphics.draw(self.sentSprite, 56, 16)
|
||||
-- engine/movie/trade.asm:751
|
||||
love.graphics.draw(self.sentSprite, 56 + self.sentSprite:getWidth(), 16, 0, -1, 1)
|
||||
if self.sentSpriteTrueColor then
|
||||
require("src.render.PaletteFX").markTrueColor(
|
||||
56 - self.scx, 16, self.sentSprite:getDimensions())
|
||||
@@ -501,7 +502,8 @@ function TradeAnim:draw()
|
||||
|
||||
elseif p == "show_enemy" then
|
||||
if self.monVisible and self.recvSprite then
|
||||
love.graphics.draw(self.recvSprite, 56, 16)
|
||||
-- engine/movie/trade.asm:751
|
||||
love.graphics.draw(self.recvSprite, 56 + self.recvSprite:getWidth(), 16, 0, -1, 1)
|
||||
if self.recvSpriteTrueColor then
|
||||
require("src.render.PaletteFX").markTrueColor(
|
||||
56, 16, self.recvSprite:getDimensions())
|
||||
|
||||
@@ -396,11 +396,10 @@ local function givePokeMon(data, speciesIndex, level, itemIndex)
|
||||
end
|
||||
|
||||
local function loadGenerated(path)
|
||||
local chunk, err = love.filesystem.load(path)
|
||||
if not chunk then return nil, err end
|
||||
local ok, value = pcall(chunk)
|
||||
if not ok then return nil, value end
|
||||
return value
|
||||
-- Same NX gold/ fallback Game2 uses. World:load is what surfaces
|
||||
-- "Gold cache incomplete" when maps.lua is invisible at the unprefixed path.
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
return CacheFs.loadActive(path)
|
||||
end
|
||||
|
||||
-- Paste the 9-tile roof sheet over atlas tiles $0a-$12.
|
||||
|
||||
Reference in New Issue
Block a user