Skip to content

Commit

Permalink
alttp: fix SRAM data reads to be more predictable and much more frequ…
Browse files Browse the repository at this point in the history
…ent; updated ROM patcher to only disallow 3 modules for receiving updates
  • Loading branch information
JamesDunne committed Feb 10, 2024
1 parent ed8622a commit 136bd32
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 51 deletions.
39 changes: 18 additions & 21 deletions games/alttp/patcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

const (
preMainLen = 0x26
preMainLen = 0x1E
// SRAM address of preMain routine called nearly every frame before `JSL GameModes`
preMainAddr = uint32(0x708000 - preMainLen)
preMainJSRAddr = uint32(0x707FFA)
Expand Down Expand Up @@ -139,32 +139,29 @@ func (p *Patcher) Patch() (err error) {
taMain.AssumeSEP(0x30)

taMain.Label("moduleCheck")

// build a reversed bitmask because our bitmask lookup table is reversed:
mask := uint16(0)
for m := range modulesOKForSync {
mask |= uint16(1 << (15 - (m - 0x07)))
}
log.Printf("mask: %04X\n", mask)

taMain.Comment("only sync during modules 07,09,0B,0E,0F,10,11,13,15,16:")
taMain.Comment("only update during safe modules:")
taMain.LDA_dp(0x10)
taMain.CMP_imm8_b(0x17)
// >= $19 : bad
taMain.CMP_imm8_b(0x19)
taMain.BCS("syncExit")
// <= $06 : bad
taMain.SBC_imm8_b(0x07 - 1)
taMain.BMI("syncExit")
taMain.ASL()
taMain.TAX()
taMain.REP(0x20)
// DungeonMask#_0098C0 is a uin16[16] of bit masks from $8000 down to $0001
taMain.LDA_long_x(0x00_98C0)
taMain.AND_imm16_w(mask)
taMain.SEP(0x20)
// == $08 : bad
taMain.CMP_imm8_b(0x08)
taMain.BEQ("syncExit")
// == $0A : bad
taMain.CMP_imm8_b(0x0A)
taMain.BEQ("syncExit")
// == $14 : bad
taMain.CMP_imm8_b(0x14)
taMain.BEQ("syncExit")
// ok:
taMain.Label("linkCheck")
taMain.Comment("don't update if Link is frozen:")
taMain.LDA_abs(0x02E4)
taMain.BNE("syncExit")

//taMain.Comment("don't update if Link is frozen:")
//taMain.LDA_abs(0x02E4)
//taMain.BNE("syncExit")

jsr := taMain.Label("syncStart")
if preMainJSRAddr != jsr+2 {
Expand Down
58 changes: 29 additions & 29 deletions games/alttp/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func (g *Game) run() {

// kick off initial WRAM read request:
g.priorityReadsMu.Lock()
q = g.enqueueWRAMReads(q)
q = g.queueReads(q)
// must always read module number LAST to validate the prior reads:
q = g.enqueueMainRead(q)
g.priorityReads[2] = q
Expand Down Expand Up @@ -195,29 +195,11 @@ func (g *Game) run() {
if stg == 0 {
// make sure a read request is always in flight to keep our main loop running:
timeSinceRead := time.Now().Sub(g.lastReadCompleted)
if timeSinceRead < time.Millisecond*512 {
// read SRAM data:
g.priorityReadsMu.Lock()
q := make([]snes.Read, 0, 12)
q = g.enqueueSRAMRead(q)

if debugSprites {
// DEBUG read sprite WRAM:
q = g.readEnqueue(q, 0xF50D00, 0xF0, 1) // [$0D00..$0DEF]
q = g.readEnqueue(q, 0xF50DF0, 0xF0, 1) // [$0DF0..$0EDF]
q = g.readEnqueue(q, 0xF50EE0, 0xC0, 1) // [$0EE0..$0F9F]
}

// must always read module number LAST to validate the prior reads:
q = g.enqueueMainRead(q)
g.priorityReads[1] = q
g.priorityReadsMu.Unlock()
} else {
if timeSinceRead > time.Millisecond*250 {
g.priorityReadsMu.Lock()
log.Printf("alttp: fastbeat: enqueue main reads; %d msec since last read\n", timeSinceRead.Milliseconds())
q := make([]snes.Read, 0, 12)
q = g.enqueueWRAMReads(q)
// must always read module number LAST to validate the prior reads:
q = g.queueReads(q)
q = g.enqueueMainRead(q)
g.priorityReads[2] = q
g.priorityReadsMu.Unlock()
Expand Down Expand Up @@ -334,6 +316,25 @@ func (g *Game) enqueueMainRead(q []snes.Read) []snes.Read {
return q
}

func (g *Game) queueReads(q []snes.Read) []snes.Read {
if g.monotonicFrameTime&7 == 7 {
// read SRAM data less frequently:
q = g.enqueueSRAMRead(q)

if debugSprites {
// DEBUG read sprite WRAM:
q = g.readEnqueue(q, 0xF50D00, 0xF0, 1) // [$0D00..$0DEF]
q = g.readEnqueue(q, 0xF50DF0, 0xF0, 1) // [$0DF0..$0EDF]
q = g.readEnqueue(q, 0xF50EE0, 0xC0, 1) // [$0EE0..$0F9F]
}
} else {
// normally read just WRAM data:
q = g.enqueueWRAMReads(q)
}

return q
}

// called when all reads are completed:
func (g *Game) readMainComplete(rsps []snes.Response) {
g.stateLock.Lock()
Expand Down Expand Up @@ -442,6 +443,11 @@ func (g *Game) readMainComplete(rsps []snes.Response) {
newFrame := g.wram[0x1A]
g.lastGameFrame = lastFrame

// should wrap around 255 to 0:
g.monotonicFrameTime++

//log.Printf("server now(): %v\n", g.ServerNow())

// assign local variables from WRAM:
local := g.LocalPlayer()

Expand Down Expand Up @@ -489,7 +495,7 @@ func (g *Game) readMainComplete(rsps []snes.Response) {
local.PriorModule = Module(g.wram[0x010C])

// only sample location during sub-module 0 for any module; keeps location more stable:
if local.SubModule == 0 {
if local.Module == 0x15 || local.SubModule == 0 {
inDungeon := g.wram[0x1B]
overworldArea := g.wramU16(0x8A)
dungeonRoom := g.wramU16(0xA0)
Expand Down Expand Up @@ -587,11 +593,6 @@ func (g *Game) readMainComplete(rsps []snes.Response) {
return
}

// should wrap around 255 to 0:
g.monotonicFrameTime++

//log.Printf("server now(): %v\n", g.ServerNow())

// handle WRAM reads:
g.readWRAM()
g.notFirstWRAMRead = true
Expand Down Expand Up @@ -689,8 +690,7 @@ func (g *Game) readMainComplete(rsps []snes.Response) {
return
}

// do all the normal WRAM reads:
q = g.enqueueWRAMReads(q)
q = g.queueReads(q)
q = g.enqueueMainRead(q)
g.priorityReads[2] = q

Expand Down
3 changes: 3 additions & 0 deletions games/alttp/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ var modulesOKForSync = map[uint8]struct{}{
0x0F: {},
0x10: {},
0x11: {},
0x12: {},
0x13: {},
0x15: {},
0x16: {},
0x17: {},
0x18: {},
}

var dungeonNames = []string{
Expand Down
2 changes: 1 addition & 1 deletion games/alttp/syncsmallkeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func (g *Game) readWRAM() {
nowTs := timestampFromTime(now)

// copy current dungeon small key counter to specific dungeon:
if g.wramFresh[0xF36F] && local.IsInDungeon() {
if g.wramFresh[0xF36F] && local.IsInDungeon() && local.SubModule == 0 {
dungeonNumber := local.Dungeon
if dungeonNumber != 0xFF && dungeonNumber < 0x20 {
currentKeyCount := g.wram[0xF36F]
Expand Down

0 comments on commit 136bd32

Please sign in to comment.