Common Derby Owners Club claims, tested against the actual arcade ROM. Each one gets a claim, the evidence, and a verdict.
Evidence, not opinion. Every offset is checkable in the real ROM.The community trades a lot of lore about how Derby Owners Club works. Some of it is true, some of it is half remembered, and some of it is just wrong. This page settles the arguments by going to the source: the game's own program ROM, disassembled and read line by line.
Each myth below has its own shareable link. Click the 🔗 next to any title, then copy the address bar to drop that exact myth into a Discord or Facebook thread.
More to come. Got one you want tested? See the bottom of the page.
"Good to Soft is a dirt-only rating. In Derby Owners Club a turf race can never show Good to Soft. If you see Good to Soft, the surface has to be dirt."
Where it shows up: community Discord and Facebook debates about how the game decides the going.
Derby Owners Club stores the track condition as a single number from 0 to 3, on one going scale, and it is the same scale for every race:
| Value | Track condition |
|---|---|
| 0 | Good |
| 1 | Good to Soft |
| 2 | Soft |
| 3 | Heavy |
That number is generated from the weather and race data, then printed by the on-screen TRACK CONDITION line. Whether the race runs on turf or dirt is stored separately, as part of the race identity; the race name itself carries TURF or DIRT. The code that sets the track condition never looks at the surface.
Four routines in the program ROM write the track condition. Every one of them sets it from weather or race data, and not one of them reads the surface:
| Routine (ROM address) | How it sets the condition | Reads surface? |
|---|---|---|
| 0x03E778 / 0x03EA32 | High nibble of a packed race-data byte | No |
| 0x03F956 | Byte copied straight from the race record | No |
| 0x054012 | Lookup in the weather-to-going table | No |
The clincher: the game's own weather-to-going table emits the Good to Soft value directly. Its outputs across all 16 entries are exactly the set {Good, Good to Soft, Soft, Heavy}, and the Good to Soft value appears at two entries:
| Table entry | Track condition produced |
|---|---|
| 2 | Soft |
| 4 | Good to Soft |
| 7 | Soft |
| 10 | Good to Soft |
| 14 | Heavy |
| all others | Good |
The input to that table is the weather and the race seed. There is no surface input anywhere in the chain. When the weather lands on those entries, the going becomes Good to Soft for the race, turf or dirt alike.
All addresses below are reproducible in the standard drbyocwc ROM set: World Edition Rev C program ROM epr-22336c.ic22, CRC32 50053F82. It is a Sega Naomi SH-4 title; the ROM image loads at 0x0C020000, so file offsets and RAM addresses line up directly.
The condition value and its string table. The condition is one global at 0x0C21A2B4 (race-info struct base 0x0C21A294, field +0x20). It indexes a four-pointer string table at file 0x150BD0:
value 0 -> "GOOD" (file 0x10EB3C) value 1 -> "GOOD TO SOFT" (file 0x10EB44) value 2 -> "SOFT" (file 0x10EB54) value 3 -> "HEAVY" (file 0x10EB5C)
The renderer at RAM 0x0C076B3A reads the value raw and indexes that table. No mask, no surface read:
0x0C076B3A mov.l 0xc076be4,r3 ; r3 = &condition (0x0C21A2B4) 0x0C076B3C mov.l 0xc076be8,r0 ; r0 = value table (0x0C170BD0) 0x0C076B3E mov.l @r3,r2 ; r2 = condition (0..3), read raw 0x0C076B40 mov.l 0xc076bec,r1 ; r1 = "TRACK CONDITION %s" 0x0C076B42 shll2 r2 ; r2 = condition * 4 0x0C076B46 mov.l @(r0,r2),r2 ; r2 = table[condition] (no surface input) 0x0C076B4A jsr @r3 ; sprintf
The four writers that set the condition, with the value each one stores:
0x03E778 / 0x03EA32 : condition = (raceData[+0x28] >> 4) & 0x0F 0x03F956 : condition = byte at raceRecord[+0x72] 0x054012 : condition = weatherGoingTable[(seed >> 3) & 0x0F]
None of them loads the surface field or branches on turf versus dirt.
The weather-to-going table at file 0x10FCA4. The going column across all 16 entries is the set {0,1,2,3}; value 1 (Good to Soft) sits at entries 4 and 10. The seed comes from the weather and RNG routine at 0x0C0B1E60:
entry 0 -> Good entry 8 -> Good
entry 1 -> Good entry 9 -> Good
entry 2 -> Soft entry 10 -> Good to Soft
entry 3 -> Good entry 11 -> Good
entry 4 -> Good to Soft entry 12 -> Good
entry 5 -> Good entry 13 -> Good
entry 6 -> Good entry 14 -> Heavy
entry 7 -> Soft entry 15 -> Good
distinct outputs = { Good, Good to Soft, Soft, Heavy }
The surface lives in the race identity: the race-name table at file 0x0C6940 carries TURF or DIRT per race. None of the four condition writers read it. Surface and going are independent fields.
Good to Soft is generated independently of the surface. The game's own going table produces it, and the routine that prints the condition never checks turf or dirt. Turf and dirt draw from the exact same mechanism, so a turf race can absolutely be Good to Soft.
Verify it yourself: open epr-22336c.ic22 (CRC32 50053F82) from the standard drbyocwc ROM set and check the offsets in the proof above.
Everything above is the World Edition Rev C build (the common drbyocwc set). The later Rev D build (World Edition EX, derbyocw) quietly rebuilt the going system, and on Rev D the answer flips: the going IS tied to the surface, and "Good to Soft" was removed from the game entirely.
Rev D uses two separate going vocabularies, one for dirt and one for turf:
| Dirt going | Turf going |
|---|---|
| Fast | Firm |
| Good | Good |
| Heavy | Soft |
| Muddy | Yielding |
So on Rev D a turf race can show Firm, Good, Soft, or Yielding, and a dirt race can show Fast, Good, Heavy, or Muddy. There is no "Good to Soft" rating on Rev D at all. The bottom line is that this is version dependent: on Rev C the going is one shared scale and a turf race can be Good to Soft, while on Rev D the going words follow the surface. That Rev D behavior is most likely where the "Good to Soft is a dirt-only rating" belief comes from.
Rev D program ROM epr-22336d.ic22, CRC32 E6C0CB0C (the derbyocw set). The on-screen condition words are drawn from one string table that is split into a dirt half and a turf half:
condition string table @0x0C173208 (8 going entries): index 0 FAST index 4 FIRM index 1 GOOD index 5 GOOD index 2 HEAVY index 6 SOFT index 3 MUDDY index 7 YIELDING indices 0-3 = the dirt going set, 4-7 = the turf going set renderer at 0x0C075676 indexes this table by the going value
The surface-only words make the split unambiguous: Fast and Muddy (dirt terms) sit in the lower half at 0 and 3, while Firm and Yielding (turf terms) sit in the upper half at 4 and 7. Rev C, by contrast, has a single four-word table (Good, Good to Soft, Soft, Heavy) with no surface input at all. The phrase "GOOD TO SOFT" exists in the Rev C ROM and is absent from the Rev D ROM.
"Draft Beer and Black Draft Beer are real foods in Derby Owners Club. Feed your horse beer and it works like any other treat: it raises its stats."
Where it shows up: stable and breeding chat, where beer splits the room. One camp says it is just a gag with no function, the other swears it boosts the horse.
Both halves of the argument are half right, so we tested each one against the ROM. The food list holds 45 items, and beer is two of them:
| Food ID | Name | Record | Growth payload |
|---|---|---|---|
| 0 | Carrot | normal 44-byte record | has stat points |
| 36 / 37 | Korean Ginseng / Large | normal record | 2,2,2,2,2,2 doubles to 4,4,4,4,4,4 |
| 43 | Draft Beer | normal record | 0,0,0,0,0,0 |
| 44 | Black Draft Beer | normal record | 0,0,0,0,0,0 |
Beer is a genuine food, not a gag. Each food is a 44-byte record with a name, an artwork pointer, and a block of stat values. Draft Beer and Black Draft Beer are complete, well-formed records, and the routine that looks a food up by its number is plain arithmetic with no "is this a real food" check. So the game reaches beer exactly the way it reaches a carrot.
But beer boosts nothing. The six stat-growth bytes in every food record are what a feed actually adds to the horse. We know these six bytes are the growth values because the "Large" version of any food carries the same six numbers scaled up: Korean Ginseng is 2,2,2,2,2,2 and Large Korean Ginseng is exactly 4,4,4,4,4,4. Beer's six bytes are all zero. It is the only food in the game with an all-zero payload. Feeding beer adds nothing to any stat.
Instead, beer is special-cased for a reaction. The stable code singles out beer by ID in three places (grouped with bananas as novelty items). Rather than running the normal feed-and-grow path, beer sets a small reaction state (Draft Beer and Black Draft Beer each flag a different one) and pulls from its own dedicated table of sequence pointers. In other words the developers wrote special code so beer does something on screen, just not a stat gain.
So is beer actually offered to you in this version? That is a separate system from the stat values, and it is where the trail goes cold in the ROM. Whether a food shows up in your feed choices is decided by a small runtime table the game builds when a session starts: the offer routine reads that table per food and offers the food when its value clears a threshold. We traced that table all the way back to the code that fills it, and found two things. First, there is no beer-specific "off" switch anywhere: not in beer's record, not in the lookup, not in the availability data, and not in the table's setup code. Beer is handled by the same general machinery as every other food. Second, the table is not a fixed list in the ROM; the game computes it at runtime. That means the one remaining question, does beer's entry land in the offerable range in this build, cannot be answered by reading the ROM alone. It has to be read from the game while it runs.
All addresses are reproducible in the standard drbyocwc ROM set: World Edition Rev C program ROM epr-22336c.ic22, CRC32 50053F82. Sega Naomi SH-4; the image loads at 0x0C020000, so file offsets and RAM addresses line up.
The food table is at 0x0C186A7C, 45 entries of 44 bytes. Each record: name (24 bytes), artwork pointer (+0x18), then the stat-growth bytes at +0x1C through +0x21. The lookup at 0x0C09AD3E is ungated arithmetic, so any ID resolves:
0x0C09AD3E mov #44,r0 ; record stride 0x0C09AD44 muls.w r0,r4 ; id * 44 0x0C09AD4A add r3,r0 ; + table base, no range check 0x0C09AD4C rts
The six growth bytes are the effect. Read across all 45 records, the +0x1C..+0x21 bytes are what differs by food, and the Large variants are the small ones scaled up:
id 36 KOREAN GINSENG 1C..21 = 02 02 02 02 02 02 id 37 LARGE KOREAN GINSENG 1C..21 = 04 04 04 04 04 04 ; exact double id 0 CARROT 1C..21 = 02 00 00 00 00 00 id 43 DRAFT BEER 1C..21 = 00 00 00 00 00 00 ; all zero id 44 BLACK DRAFT BEER 1C..21 = 00 00 00 00 00 00 ; all zero
Beer is special-cased by ID at three stable-code sites (each tests both 43 and 44, with no food-count bound, so these are beer-only branches):
0x0C0A4110 draw path : beer uses hardcoded display offsets
(0x764 / 0x790) into a separate buffer,
not the normal table + id*44 indexing
0x0C0A5B44 feed path : bananas (41/42) + beer (43/44) share a
branch; beer writes a reaction state at
0x0C21A4F4 (draft=1, black draft=2)
0x0C0A5FA0 feed path : beer indexes its own pointer tables at
0x0C187E60 / 0x0C187E8C (its reaction
sequence), bypassing normal handling
The feed routine never reads the growth bytes for beer; it runs the reaction path above. Normal foods fall through to the usual grow path.
The offer gate (the access question). Whether a food is offered is decided here, independently of the growth bytes:
0x0C04806C offer builder : for food 0..44, read the runtime
array at 0x0C21D738 + food*4, look it up
in the availability table, and offer the
food when the result is < 3
0x0C09AD50 lookup : reads AVAIL[value*16 + mode_column]
0x0C1467FC AVAIL table : 16 bytes per row; beer's rows are
normal and offerable, not gated
Who fills the runtime array (this was the wall). The array at 0x0C21D738 is care-state base 0x0C21D6B8 + 0x80. It is filled by the care-state init:
0x0C03563C init fill loop : writes 0x0C21D738[index] for the
first 40 foods; each entry is the
return of a runtime divide routine
(0x0C0C211C), not a value copied from
a fixed table in the ROM
beer entries : DRAFT BEER -> 0x0C21D7E4 (0x0C21D738 + 43*4)
BLACK DRAFT BEER -> 0x0C21D7E8 (0x0C21D738 + 44*4)
Because the values are computed live, the ROM does not state beer's final offer value. Reading those two RAM addresses while the game runs is what closes it.
Here is what the ROM settles for certain. Beer is not a gag: Draft Beer and Black Draft Beer are complete, normal food records, and the game even writes special code just for them. And beer boosts nothing: its six growth values are all zero, the only food in the game like that. There is also no beer-specific "off" switch anywhere we looked.
What is still open is whether beer actually appears in your feed choices in World Edition Rev C. The game builds that decision from a small table it computes while running, not from a fixed list in the ROM, so reading the cartridge alone cannot answer it. This is where a community hand would finish the job:
0x0C21D7E4 (Draft Beer) and 0x0C21D7E8 (Black Draft Beer). A value under 3 means the game considers that beer offerable in the current mode.0x0C0C211C filled from 0x0C03563C. Working out its inputs would give a static answer for beer without needing a live read.+0x1C..+0x21 are applied to the horse by the training code. Mapping each byte to an on-screen stat (Speed, Stamina, and so on) would tell us exactly what beer gives zero of.Everything above is reproducible from epr-22336c.ic22 (CRC32 50053F82). If you take any of these further, bring it to the Discord or the Facebook group below and it will be folded in here with credit.