Merge pull request #2512 from 1Jamie/grandpas-garage

fix: enforce bike dismount on indoor maps and synchronize biking state across saves
This commit is contained in:
bryanthaboi
2026-09-26 20:31:10 -04:00
committed by GitHub
8 changed files with 168 additions and 30 deletions
+37 -16
View File
@@ -431,28 +431,49 @@ end
-- pokefirered/src/item_use.c:253 FieldUseFunc_Bike
function ItemUse.useBike(session)
local Player = require("src.core.game3.player")
-- pokefirered/src/item_use.c:261
if Player.biking and Player.isOnCyclingRoad and Player.isOnCyclingRoad(session) then
return false, "bike", cant_dismount_bike_text()
end
-- pokefirered/src/overworld.c:948 Overworld_IsBikingAllowed
local biking = map_header_flag(session, "bikingAllowed")
if biking == nil then biking = is_outdoor(session) end
if not biking then
return false, "bike", not_the_time(session)
end
-- pokefirered/src/item_use.c:276 ItemUseOnFieldCB_Bicycle
if not Player.biking then
if Player.biking then
-- pokefirered/src/item_use.c:261: If already on bike, cannot dismount on cycling road
if Player.isOnCyclingRoad and Player.isOnCyclingRoad(session) then
return false, "bike", cant_dismount_bike_text()
end
-- Dismounting is always allowed elsewhere (even if indoors)
Player.biking = false
if session then session.biking = false end
local Runtime = package.loaded["src.core.game3.runtime"]
local curSession = Runtime and Runtime.getSession and Runtime.getSession()
if curSession then curSession.biking = false end
local game = Runtime and Runtime.getGame and Runtime.getGame()
if game and game.save then
game.save.biking = false
if game.save.position then game.save.position.biking = false end
end
require("src.core.game3.audio").bikeMusic(false)
return true, "bike", nil
else
-- pokefirered/src/overworld.c:948 Overworld_IsBikingAllowed: Mounting only allowed where biking is permitted
local biking = map_header_flag(session, "bikingAllowed")
if biking == nil then biking = is_outdoor(session) end
if not biking then
return false, "bike", not_the_time(session)
end
pcall(function()
local Audio = require("src.core.game3.audio")
local SE = require("src.core.game3.se_ids")
if Audio and Audio.playSe then Audio.playSe(SE.SE_BIKE_BELL) end
end)
Player.biking = true
if session then session.biking = true end
local Runtime = package.loaded["src.core.game3.runtime"]
local curSession = Runtime and Runtime.getSession and Runtime.getSession()
if curSession then curSession.biking = true end
local game = Runtime and Runtime.getGame and Runtime.getGame()
if game and game.save then
game.save.biking = true
if game.save.position then game.save.position.biking = true end
end
require("src.core.game3.audio").bikeMusic(true)
return true, "bike", nil
end
Player.biking = not Player.biking
require("src.core.game3.audio").bikeMusic(Player.biking)
return true, "bike", nil
end
--- Check TM pre-flight compatibility and known moves matching retail FRLG.
+6 -7
View File
@@ -326,12 +326,14 @@ function Map.load(mod, game, mapId, opts)
end
end
local session = Runtime.getSession and Runtime.getSession()
local session = (Runtime.getSession and Runtime.getSession()) or (game and game.session)
local save = game and game.save
local Player = require("src.core.game3.player")
local onCyclingRoad = Player.isOnCyclingRoad and Player.isOnCyclingRoad(session, x, y)
local wasBiking = Player.biking or (session and session.biking == true)
or (save and save.biking == true)
local wasBiking = (Player.biking == true)
if opts.initialLoad and not wasBiking then
wasBiking = (session and session.biking == true) or (save and save.biking == true) or false
end
-- pokefirered/src/overworld.c:878 GetAdjustedInitialTransitionFlags
local keepBike = false
@@ -347,7 +349,6 @@ function Map.load(mod, game, mapId, opts)
if onCyclingRoad and not (Player.surfing or Player.surfHopping) then
keepBike = true
end
Player.biking = keepBike
end
if session then
@@ -365,6 +366,7 @@ function Map.load(mod, game, mapId, opts)
save.position.x = x
save.position.y = y
save.position.facing = facing
save.position.biking = keepBike
save.biking = keepBike
end
@@ -380,9 +382,6 @@ function Map.load(mod, game, mapId, opts)
Player.reset(x, y, facing)
end
-- pokefirered/src/overworld.c:2145 SetPlayerAvatarTransitionFlags
if onCyclingRoad and not (Player.surfing or Player.surfHopping) then
keepBike = true
end
Player.biking = keepBike
Player.syncSavePosition(game)
+14 -2
View File
@@ -170,6 +170,9 @@ function Player.syncFromSession(session)
if session.elevation ~= nil then
Player.elevation = tonumber(session.elevation) or 3
end
if session.biking ~= nil then
Player.biking = (session.biking == true)
end
end
function Player.syncFromHost(game)
@@ -180,6 +183,10 @@ function Player.syncFromHost(game)
if p.elevation ~= nil then
Player.elevation = tonumber(p.elevation) or 3
end
local save = game and game.save
if save and save.biking ~= nil then
Player.biking = (save.biking == true)
end
end
--- Write avatar coords into save.position (ferry / host save). No host entity mirror.
@@ -189,10 +196,15 @@ function Player.syncSavePosition(game)
save.position.x = Player.cellX
save.position.y = Player.cellY
save.position.facing = Player.facing
save.position.biking = (Player.biking == true)
save.biking = (Player.biking == true)
local session = package.loaded["src.core.game3.runtime"]
session = session and session.getSession and session.getSession()
if session and session.map then
save.position.map = session.map
if session then
session.biking = (Player.biking == true)
if session.map then
save.position.map = session.map
end
end
end
+26
View File
@@ -193,9 +193,35 @@ function Runtime.start(mod, game, session, opts)
if opts.alreadyOnMap then
-- Stay on current host map; only ensure Space VM + depth-1 neighbors.
local Map = require("src.core.game3.map")
local Player = require("src.core.game3.player")
local def = game and game.data and game.data.maps and game.data.maps[session.map]
Map.current = session.map
Map.loadNeighborsDepth1(game, def)
-- Enforce overworld biking permissions when adopting / resuming an existing map.
-- pokefirered/src/overworld.c:878 GetAdjustedInitialTransitionFlags
local onCyclingRoad = Player.isOnCyclingRoad and Player.isOnCyclingRoad(session, Player.cellX, Player.cellY)
local wasBiking = (Player.biking == true) or (session and session.biking == true) or (game and game.save and game.save.biking == true)
local keepBike = false
if wasBiking or onCyclingRoad then
local allowed = def and def.bikingAllowed
if allowed ~= nil then
keepBike = (tonumber(allowed) or 0) ~= 0
else
local pair = def and (def.pair or (def.midLayout and def.midLayout.pair))
keepBike = type(pair) == "string" and pair:find("outdoor", 1, true) ~= nil
end
if onCyclingRoad and not (Player.surfing or Player.surfHopping) then
keepBike = true
end
end
Player.biking = keepBike
if session then session.biking = keepBike end
if game and game.save then
game.save.biking = keepBike
if game.save.position then game.save.position.biking = keepBike end
end
Player.syncSavePosition(game)
-- Space.onMapEnter already ran (or will run) from afterMap — don't double.
log("adopted existing game3 map (no re-warp)")
else
+3 -2
View File
@@ -332,8 +332,9 @@ function Schema.toSaveTable(session)
x = session.x,
y = session.y,
facing = session.facing,
biking = (session.biking == true)
or (package.loaded["src.core.game3.player"] and package.loaded["src.core.game3.player"].biking == true)
biking = (package.loaded["src.core.game3.player"] and package.loaded["src.core.game3.player"].biking ~= nil)
and (package.loaded["src.core.game3.player"].biking == true)
or (session and session.biking == true)
or false,
healMap = session.healMap,
healX = session.healX,
@@ -123,6 +123,8 @@ Events.HANDLERS = {
if okP and Player and not Player.surfing then
Player.biking = true
Player.surfHopping = false
local game = package.loaded["src.core.game3.runtime"] and package.loaded["src.core.game3.runtime"]._game
Player.syncSavePosition(game)
end
require("src.core.game3.audio").bikeMusic(true, true)
return false
@@ -134,6 +136,8 @@ Events.HANDLERS = {
Player.surfing = true
Player.biking = false
Player.surfHopping = false
local game = package.loaded["src.core.game3.runtime"] and package.loaded["src.core.game3.runtime"]._game
Player.syncSavePosition(game)
end
return false
end,
+3 -3
View File
@@ -88,13 +88,13 @@ end
-- is why nil covers PLAYER_SURF without a test of its own.
function Bike.tryBike(ctx)
ctx = ctx or {}
if not Bike.canUseHere(ctx.environment, ctx.collision) then return nil end
local state = ctx.state or FieldMoves.PLAYER_NORMAL
if state == FieldMoves.PLAYER_NORMAL then return "mount" end
if state == FieldMoves.PLAYER_BIKE then
if FieldMoves.isBiking(state) or state == Bike.PLAYER_BIKE_ID or state == 1 then
if ctx.alwaysOnBike then return "cant_get_off" end
return "dismount"
end
if not Bike.canUseHere(ctx.environment, ctx.collision) then return nil end
if state == FieldMoves.PLAYER_NORMAL or state == Bike.PLAYER_NORMAL_ID or state == 0 then return "mount" end
return nil
end
+75
View File
@@ -147,6 +147,81 @@ check(saveTable.biking == true, "Schema.toSaveTable includes biking = true")
local restoredSession = Schema.fromSaveTable(saveTable)
check(restoredSession.biking == true, "Schema.fromSaveTable restores biking = true")
-- 10. Test dismounting on normal land and moving between outdoor maps keeps player on foot
Flags.setFlag(session.store, nil, 0x830, false)
session.flags[0x830] = false
Player.biking = true
ItemUse.useBike(session)
check(Player.biking == false, "dismounted bike on normal land")
local palletDef = {
id = "FR_PALLET_TOWN",
bikingAllowed = 1,
pair = "outdoor",
}
local route1Def = {
id = "FR_ROUTE_1",
bikingAllowed = 1,
pair = "outdoor",
}
game.data.maps.FR_PALLET_TOWN = palletDef
game.data.maps.FR_ROUTE_1 = route1Def
Map.load(nil, game, "FR_ROUTE_1", { x = 10, y = 35, facing = "up" })
check(Player.biking == false, "moving from Pallet Town to Route 1 while on foot leaves Player on foot")
Map.load(nil, game, "FR_PALLET_TOWN", { x = 10, y = 0, facing = "down" })
check(Player.biking == false, "moving back to Pallet Town leaves Player on foot")
-- 11. Test saving while on foot preserves biking = false
local onFootSave = Schema.toSaveTable(session)
check(onFootSave.biking == false, "Schema.toSaveTable records biking = false when on foot")
local restoredFoot = Schema.fromSaveTable(onFootSave)
check(restoredFoot.biking == false, "Schema.fromSaveTable restores biking = false when on foot")
-- 12. Test entering indoor map (e.g. Silph Co) while biking forces dismount to foot
local silphDef = {
id = "FR_SILPH_CO_1F",
bikingAllowed = 0,
pair = "indoor",
}
local saffronDef = {
id = "FR_SAFFRON_CITY",
bikingAllowed = 1,
pair = "outdoor",
}
game.data.maps.FR_SILPH_CO_1F = silphDef
game.data.maps.FR_SAFFRON_CITY = saffronDef
session = Runtime.getSession() or session
Player.biking = true
session.biking = true
Map.load(nil, game, "FR_SILPH_CO_1F", { x = 18, y = 21, facing = "up" })
check(Player.biking == false, "entering Silph Co 1F forces Player.biking = false")
check(session.biking == false, "session.biking is false in Silph Co")
-- Exiting Silph Co back to Saffron City keeps player on foot (not re-mounted)
Map.load(nil, game, "FR_SAFFRON_CITY", { x = 18, y = 22, facing = "down" })
check(Player.biking == false, "exiting Silph Co to Saffron City keeps Player on foot")
-- 13. Test loading directly into Silph Co via alreadyOnMap forces dismount to foot
Player.biking = true
session.biking = true
session.map = "FR_SILPH_CO_1F"
game.save = { biking = true, position = { map = "FR_SILPH_CO_1F", biking = true } }
Runtime.start(nil, game, session, { alreadyOnMap = true })
check(Player.biking == false, "loading directly into Silph Co with alreadyOnMap clears Player.biking")
check(session.biking == false, "session.biking cleared to false on Silph Co load")
-- 14. Test Gen 2 Bike.tryBike allows dismounting when in indoor environment
local Gen2Bike = require("src.world.gen2.Bike")
local dismountAction = Gen2Bike.tryBike({
state = 1, -- PLAYER_BIKE
environment = "INDOOR",
collision = 0,
alwaysOnBike = false,
})
check(dismountAction == "dismount", "Gen 2 Bike.tryBike allows dismount in INDOOR environment")
print(string.format("=== RESULTS: %d passed, %d failed ===", passed, failed))
if failed > 0 then os.exit(1) end