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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to Light Engine will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.16-advanced.8] - 2026-10-04

### Fixed
- Destroyed menu-repeat controls are removed and safely recycled.
- Desktop overlay HTTP timeouts end the request without a second retry.
- Menu navigation consumes a quick direction tap once, including presses that
end before an update. Held keys retain the existing repeat delay.
- Portrait windows retain the full logical game viewport and letterbox it,
keeping menus and gameplay visible when Android rotates the device.

Android version code: 124.

## [0.1.16-advanced.7] - 2026-10-04

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions funkin/backend/throttle.lua
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ function ThrottleObject:check()
end

function ThrottleObject:destroy()
table.destroy(Throttle.list, self)
table.insert(dead, setmetatable(self))
if not table.delete(Throttle.list, self) then return end
table.insert(dead, setmetatable(self, nil))
end

-- Throttle
Expand Down
14 changes: 9 additions & 5 deletions funkin/ui/menulist.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
local MenuList = SpriteGroup:extend("MenuList")
MenuList.selectionCache = {}

local function navigationInput(name)
return controls:pressed(name) or controls:down(name)
end

local defaultScrolls = {
default = function(self, obj, dt, time)
obj.y = self:lerp(dt, obj, "y", time)
Expand Down Expand Up @@ -83,11 +87,11 @@ function MenuList:new(sound, cache, scroll, hover)

self.throttles = {}
if self.scroll ~= "horizontal" and not self.isMobile then
self.throttles[-1] = Throttle:make({controls.down, controls, "ui_up"})
self.throttles[1] = Throttle:make({controls.down, controls, "ui_down"})
self.throttles[-1] = Throttle:make({navigationInput, "ui_up"})
self.throttles[1] = Throttle:make({navigationInput, "ui_down"})
elseif self.scroll == "horizontal" and not self.isMobile then
self.throttles[-1] = Throttle:make({controls.down, controls, "ui_left"})
self.throttles[1] = Throttle:make({controls.down, controls, "ui_right"})
self.throttles[-1] = Throttle:make({navigationInput, "ui_left"})
self.throttles[1] = Throttle:make({navigationInput, "ui_right"})
end
end

Expand Down Expand Up @@ -322,4 +326,4 @@ function MenuList:_getBoundary()
self.origin.x, self.origin.y
end

return MenuList
return MenuList
4 changes: 2 additions & 2 deletions loxel/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ function game.init(app, state, ...)
local width, height = app.width, app.height
if app.adaptableWidth then
local sw, sh = love.graphics.getDimensions()
if sw > 0 and sh > 0 then width = math.max(1, math.floor(height * sw / sh)) end
if sw > 0 and sh > 0 and sw >= sh then width = math.max(1, math.floor(height * sw / sh)) end
end
game.width, game.height = width, height
Display.refresh()
Expand Down Expand Up @@ -405,7 +405,7 @@ function game.resize(w, h)
if Project.adaptableWidth then
local oldWidth, oldHeight = game.width, game.height
if oldHeight > 0 then
game.width = math.max(1, math.floor(oldHeight * w / h))
game.width = w < h and Project.width or math.max(1, math.floor(oldHeight * w / h))
for _, camera in ipairs(game.cameras.list) do
if camera.x == 0 and camera.y == 0 and camera.width == oldWidth and camera.height == oldHeight then
camera:resize(game.width, oldHeight, camera.resolutionX, camera.resolutionY)
Expand Down
4 changes: 2 additions & 2 deletions project.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ return {
title = "Light Engine",
file = "Light Engine",
icon = "art/logo.png",
version = "0.1.16-advanced.7",
androidVersionCode = 123,
version = "0.1.16-advanced.8",
androidVersionCode = 124,
package = "com.zyn.lightengine",
width = 1280,
height = 720,
Expand Down
2 changes: 1 addition & 1 deletion separate-addons/screen-overlay/modules/overlay.lua
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ while true do
if success then code,body,reason=a,b,c else code,body,reason=nil,'',a end
end
local pending=((connecting and code and code>=200 and code<300)or code==403)and body:find('"pending"%s*:%s*true')
local startup=code==nil and love.timer.getTime()-started<3
local startup=code==nil and reason~='timeout' and love.timer.getTime()-started<3
if not pending and not startup then break end
if task.close or cancel:peek() or tasks:peek() or love.timer.getTime()-started>=30 then break end
local untilTime=love.timer.getTime()+.5
Expand Down
8 changes: 5 additions & 3 deletions tests/engine/compat-controls.lua
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,19 @@ function love.load()
assert(state.camHUD.__shakeDuration==0 and state.camHUD.__fadeDuration==0,'owned camera effects survived disposal')
assert(not pcall(cleanupFont.getHeight,cleanupFont) and pcall(cachedFont.getHeight,cachedFont),'font ownership cleanup incorrect')
local destroyedCamera=Camera();state.camOther=destroyedCamera
local weakContext
do
local function disposedContext()
local abandonedScript=Script('tests/engine/compat-fixture.lua',false,true,true)
local abandoned=Script.createCompatibility(state,abandonedScript)
abandonedScript.variables.requestAsset('image','test-controls','abandoned')
abandonedScript.variables.cameraFlash('other','FFFFFF',5,true,'destroyedCamera')
destroyedCamera:destroy()
abandoned:dispose();abandonedScript:close()
assert(destroyedCamera.__flashComplete==nil,'destroyed camera retained owned completion closure')
weakContext=setmetatable({abandoned},{__mode='v'})
return setmetatable({abandoned},{__mode='v'})
end
-- End the allocating stack frame before checking reachability; LuaJIT may
-- retain registers from a lexical block until its enclosing function returns.
local weakContext=disposedContext()
collectgarbage('collect')
assert(weakContext[1]==nil,'pending callback retained a disposed context/state')
while paths.async.getProgress()<1 do assert(love.timer.getTime()<deadline);paths.async.update(.01);love.timer.sleep(.002) end
Expand Down
19 changes: 19 additions & 0 deletions tests/engine/core-utilities.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,25 @@ local function check(name, test)
end

function love.load()
check('destroyed navigation throttles can be reused', function()
local throttle = require 'funkin.backend.throttle'
local first = throttle:make({function() return false end})
local destroy = first.destroy
first:destroy()
assert(not table.find(throttle.list, first), 'destroyed throttle remained active')
local second = throttle:make({function() return true end})
assert(second == first, 'destroyed throttle was not recycled')
second:update(0.016)
assert(second:check(), 'recycled throttle lost its methods or input')
second:destroy()
-- A repeated destroy must not insert the same object into the pool twice.
destroy(second)
local third = throttle:make({function() return false end})
local fourth = throttle:make({function() return false end})
assert(third ~= fourth, 'repeated destruction recycled one throttle twice')
third:destroy()
fourth:destroy()
end)
check('keyboard taps keep both edges until the frame ends', function()
local keyboard = require 'loxel.input.keyboard'
keyboard.onPressed('a')
Expand Down
10 changes: 10 additions & 0 deletions tests/engine/display.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ local function mode(w,h)
end
local function near(a,b) assert(math.abs(a-b)<.001, tostring(a)..' ~= '..tostring(b)) end
function love.load()
check('portrait adaptive resize retains a usable game viewport',function()
mode(360,640);Project.adaptableWidth=true
game.resize(360,640)
assert(game.width==Project.width and game.height==Project.height,'portrait orientation cropped the logical game screen')
local view=game.display.getViewport()
near(view.width,360);near(view.height,202.5)
local x,y=game.display.gameToWindow(640,360)
near(x,180);near(y,320)
Project.adaptableWidth=false
end)
check('adaptive OS resize preserves native size and project configuration',function()
mode(640,480); local original=Project.width; Project.adaptableWidth=true
game.resize(640,480)
Expand Down
26 changes: 26 additions & 0 deletions tests/engine/gameplay-flow.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
check('quick menu taps move once and accept the selected item', function()
for _, navigation in ipairs({{'vertical', 's'}, {'horizontal', 'd'}}) do
local menu = MenuList(nil, false, navigation[1])
for _ = 1, 3 do menu:add(Sprite()) end
controls:onKeyPress(navigation[2])
controls:onKeyRelease(navigation[2])
controls:update()
Throttle:update(0.016)
menu:update(0.016)
assert(menu.curSelected == 2, 'a quick direction tap did not move the menu')
controls:update()
Throttle:update(0.016)
menu:update(0.016)
assert(menu.curSelected == 2, 'one direction tap moved twice')
local selected, calls = nil, 0
menu.selectCallback = function(item) selected = item; calls = calls + 1 end
controls:onKeyPress('return')
controls:onKeyRelease('return')
controls:update()
menu:update(0.016)
assert(selected == menu.members[2] and calls == 1, 'a quick accept tap did not select the second item')
for _, throttle in pairs(menu.throttles) do throttle:destroy() end
menu:destroy()
end
end)

check('keyboard hits, misses and pause preserve a native song and mod HUD', function()
ClientPrefs.data.botplayMode = false
ClientPrefs.data.ghostTap = false
Expand Down
Loading