popn: add io emulation for more lights and coin (#637)

## Link to GitHub Issue or related Pull Request, if one exists
#618 

## Description of change
Wire up coin and more lights to bi3a_hook.

## Testing
Tested on U region
This commit is contained in:
bicarus
2026-04-14 09:46:13 -07:00
committed by GitHub
parent e59a50b64c
commit 0fc430ceb3
6 changed files with 269 additions and 102 deletions
+73 -23
View File
@@ -1,6 +1,6 @@
#include "bi3a_hook.h" #include "bi3a_hook.h"
#if SPICE64 #if SPICE64 && !SPICE_XP
#include <optional> #include <optional>
#include <cstdint> #include <cstdint>
@@ -9,6 +9,7 @@
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "games/io.h" #include "games/io.h"
#include "popn.h"
#include "io.h" #include "io.h"
#include "util/tapeled.h" #include "util/tapeled.h"
#include <typeinfo> #include <typeinfo>
@@ -54,17 +55,20 @@ namespace games::popn {
union AIO_IOB5_BI3A__DEVSTATUS { union AIO_IOB5_BI3A__DEVSTATUS {
uint8_t RawBytes[648]; uint8_t RawBytes[648];
struct { struct {
uint8_t Reserved0[44]; // 0-43 uint8_t Reserved0[27]; // 0-26
uint8_t Coin0; // 27
uint8_t Coin1; // 28
uint8_t Reserved1[15]; // 29-43
uint8_t TestButton; // 44, io code offset rcx+0xe4 uint8_t TestButton; // 44, io code offset rcx+0xe4
uint8_t ServiceButton; // 45, io code offset rcx+0xe5 uint8_t ServiceButton; // 45, io code offset rcx+0xe5
uint8_t CoinMech; // 46, io code offset rcx+0xe6 uint8_t CoinMech; // 46, io code offset rcx+0xe6
uint8_t Reserved1[19]; // 47-65 uint8_t Reserved2[19]; // 47-65
uint8_t JOYL_SW; // 66 uint8_t JOYL_SW; // 66
uint8_t Reserved2[4]; // 67-70 uint8_t Reserved3[4]; // 67-70
uint8_t JOYR_SW; // 71 uint8_t JOYR_SW; // 71
uint8_t Headphones; // 72 uint8_t Headphones; // 72
uint8_t Recorder; // 73, unused uint8_t Recorder; // 73, unused
uint8_t Reserved3[295]; // 74-368 uint8_t Reserved4[295]; // 74-368
uint8_t Buttons_0_8; // 369 uint8_t Buttons_0_8; // 369
uint8_t Button_9; // 370 uint8_t Button_9; // 370
} Input; } Input;
@@ -244,7 +248,7 @@ namespace games::popn {
if (i_pSci == aioSciComm) { if (i_pSci == aioSciComm) {
aioNmgrIob5 = new AIO_NMGR_IOB5; aioNmgrIob5 = new AIO_NMGR_IOB5;
log_info("bi2x_hook", "aioNMgrIob5_Create called, returning custom AIO_NMGR_IOB5: {}", fmt::ptr(aioNmgrIob5)); log_info("bi3a_hook", "aioNMgrIob5_Create called, returning custom AIO_NMGR_IOB5: {}", fmt::ptr(aioNmgrIob5));
return aioNmgrIob5; return aioNmgrIob5;
} else { } else {
return aioNMgrIob5_Create_orig(i_pSci, i_bfMode); return aioNMgrIob5_Create_orig(i_pSci, i_bfMode);
@@ -253,11 +257,11 @@ namespace games::popn {
static AIO_IOB5_BI3A *__fastcall aioIob5Bi3a_Create(AIO_NMGR_IOB5 *i_pNodeMgr, uint8_t i, void *p) { static AIO_IOB5_BI3A *__fastcall aioIob5Bi3a_Create(AIO_NMGR_IOB5 *i_pNodeMgr, uint8_t i, void *p) {
log_info("bi2x_hook", "aioIob5Bi3a_Create called with i_pNodeMgr={}, i={}, p={}", fmt::ptr(i_pNodeMgr), i, fmt::ptr(p)); log_info("bi3a_hook", "aioIob5Bi3a_Create called with i_pNodeMgr={}, i={}, p={}", fmt::ptr(i_pNodeMgr), i, fmt::ptr(p));
if (i_pNodeMgr == aioNmgrIob5) { if (i_pNodeMgr == aioNmgrIob5) {
aioIob5Bi3a = new AIO_IOB5_BI3A; aioIob5Bi3a = new AIO_IOB5_BI3A;
log_info("bi2x_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a)); log_info("bi3a_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a));
return aioIob5Bi3a; return aioIob5Bi3a;
} else { } else {
return aioIob5Bi3a_Create_orig(i_pNodeMgr, i, p); return aioIob5Bi3a_Create_orig(i_pNodeMgr, i, p);
@@ -326,7 +330,8 @@ namespace games::popn {
o_DevStatus->Input.Headphones = 1; o_DevStatus->Input.Headphones = 1;
} }
// TODO: coin // coin
o_DevStatus->Input.Coin0 += eamuse_coin_get_stock();
} }
static void __fastcall aioIob5Bi3a_SetOutputData(AIO_IOB5_BI3A *i_pNodeCtl, uint32_t i_CnPin, uint8_t i_Data) { static void __fastcall aioIob5Bi3a_SetOutputData(AIO_IOB5_BI3A *i_pNodeCtl, uint32_t i_CnPin, uint8_t i_Data) {
@@ -344,6 +349,15 @@ namespace games::popn {
std::optional<Lights::popn_lights_t> light; std::optional<Lights::popn_lights_t> light;
switch (i_CnPin) { switch (i_CnPin) {
case 7:
light = Lights::IC_Card_R;
break;
case 8:
light = Lights::IC_Card_G;
break;
case 9:
light = Lights::IC_Card_B;
break;
case 10: case 10:
light = Lights::WooferLED_R; light = Lights::WooferLED_R;
break; break;
@@ -376,9 +390,10 @@ namespace games::popn {
struct PopnLight { struct PopnLight {
int data_index; int data_index;
Lights::popn_lights_t light; Lights::popn_lights_t light;
uint8_t size;
PopnLight( PopnLight(
int data_index, Lights::popn_lights_t light) : int data_index, Lights::popn_lights_t light, uint8_t size) :
data_index(data_index), light(light) {} data_index(data_index), light(light), size(size) {}
}; };
static void __fastcall aioIob5Bi3a_SetTapeLedDataPart( static void __fastcall aioIob5Bi3a_SetTapeLedDataPart(
@@ -403,15 +418,14 @@ namespace games::popn {
log_info( log_info(
"bi3a_hook", "bi3a_hook",
"aioIob5Bi3a_SetTapeLedDataPart Pin={}, Size={}, data@{} = {}", "aioIob5Bi3a_SetTapeLedDataPart Pin={}, Size={}, Type={}, data@{} = {}",
i_CnPin, number_of_leds, fmt::ptr(i_pData), data); i_CnPin, number_of_leds, (uint8_t)i_LedType, fmt::ptr(i_pData), data);
} }
auto &lights = get_lights(); auto &lights = get_lights();
if (i_CnPin == 0 && number_of_leds == 9 * 3) {
// special handling converting to non-RGB lights // special handling converting to non-RGB lights
// take the max(R, G, B) and use it to write the light value // take the max(R, G, B) and use it to write the light value
if (i_CnPin == 0 && number_of_leds == 9 * 3) {
uint8_t light_value[9] = { 0 }; uint8_t light_value[9] = { 0 };
for (uint32_t i = 0; i < number_of_leds; i += 3) { for (uint32_t i = 0; i < number_of_leds; i += 3) {
light_value[i / 3] = light_value[i / 3] =
@@ -430,6 +444,8 @@ namespace games::popn {
Lights::popn_lights_t::Button9 Lights::popn_lights_t::Button9
}; };
static_assert(std::size(legacy_button_lights) == 9);
for (size_t light = 0; light < 9; light++) { for (size_t light = 0; light < 9; light++) {
// on the new cab, buttons are colorless plastic and they rely on RGB to be lit at all times, even when "off" // on the new cab, buttons are colorless plastic and they rely on RGB to be lit at all times, even when "off"
// when translating to legacy on/off lights, treat anything above ~60% brightness as fully on, // when translating to legacy on/off lights, treat anything above ~60% brightness as fully on,
@@ -439,16 +455,50 @@ namespace games::popn {
lights.at(legacy_button_lights[light]), lights.at(legacy_button_lights[light]),
light_value[light] > 150 ? 1.f : 0.f); light_value[light] > 150 ? 1.f : 0.f);
} }
// color buttons
constexpr Lights::popn_lights_t button_lights[] = {
Lights::popn_lights_t::PikaButton1_R, Lights::popn_lights_t::PikaButton1_G, Lights::popn_lights_t::PikaButton1_B,
Lights::popn_lights_t::PikaButton2_R, Lights::popn_lights_t::PikaButton2_G, Lights::popn_lights_t::PikaButton2_B,
Lights::popn_lights_t::PikaButton3_R, Lights::popn_lights_t::PikaButton3_G, Lights::popn_lights_t::PikaButton3_B,
Lights::popn_lights_t::PikaButton4_R, Lights::popn_lights_t::PikaButton4_G, Lights::popn_lights_t::PikaButton4_B,
Lights::popn_lights_t::PikaButton5_R, Lights::popn_lights_t::PikaButton5_G, Lights::popn_lights_t::PikaButton5_B,
Lights::popn_lights_t::PikaButton6_R, Lights::popn_lights_t::PikaButton6_G, Lights::popn_lights_t::PikaButton6_B,
Lights::popn_lights_t::PikaButton7_R, Lights::popn_lights_t::PikaButton7_G, Lights::popn_lights_t::PikaButton7_B,
Lights::popn_lights_t::PikaButton8_R, Lights::popn_lights_t::PikaButton8_G, Lights::popn_lights_t::PikaButton8_B,
Lights::popn_lights_t::PikaButton9_R, Lights::popn_lights_t::PikaButton9_G, Lights::popn_lights_t::PikaButton9_B
};
static_assert(std::size(button_lights) == 9 * 3);
for (size_t light = 0; light < number_of_leds; light++) {
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(button_lights[light]),
((uint8_t *)i_pData)[light] / 255.f);
}
} }
// pin 0 = buttons 1-3, size 9 (but it's actually triple that in reality - game passes in wrong length), values 0-0xFF if (tapeledutils::is_enabled() && i_CnPin > 0 && i_CnPin < std::size(TAPELED_MAPPING)) {
// pin 1 = left pop (red pop kun), size 16, values 0-0xFF auto &map = TAPELED_MAPPING[i_CnPin];
// pin 2 = right pop (blue pop kun), size 16, values 0-0xFF const auto data_size = std::min(map.data.capacity(), (size_t)number_of_leds / 3);
// pin 3 = title, size 44, values 0-0xFF
// pin 4 = speaker, size 40, values 0-0xFF - U region only // pick a color to use
// pin 5 = monitor, size 18, values 0-0x7F - U region only const auto rgb = tapeledutils::pick_color_from_led_tape((uint8_t *)i_pData, data_size);
// pin 6 = control panel, size 28, values 0-0x7F - U region only
// pin 7 = cabinet, size 22, values 0-0x7F - U region only // program the lights into API
auto &lights = get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
// tape LED output over API not implemented
// for (size_t i = 0; i < data_size; i++) {
// map.data[i].r = ((uint8_t *)i_pData)[i * 3];
// map.data[i].g = ((uint8_t *)i_pData)[i * 3 + 1];
// map.data[i].b = ((uint8_t *)i_pData)[i * 3 + 2];
// }
}
} }
void bi3a_hook_init() { void bi3a_hook_init() {
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once #pragma once
#if SPICE64 #if SPICE64 && !SPICE_XP
namespace games::popn { namespace games::popn {
void bi3a_hook_init(); void bi3a_hook_init();
+94 -59
View File
@@ -43,65 +43,100 @@ std::vector<Light> &games::popn::get_lights() {
if (lights.empty()) { if (lights.empty()) {
lights = GameAPI::Lights::getLights("Pop'n Music"); lights = GameAPI::Lights::getLights("Pop'n Music");
GameAPI::Lights::sortLights( GameAPI::Lights::sortLightsWithCategory(&lights, {
&lights, {"Common", "Button 1"},
"Button 1", {"Common", "Button 2"},
"Button 2", {"Common", "Button 3"},
"Button 3", {"Common", "Button 4"},
"Button 4", {"Common", "Button 5"},
"Button 5", {"Common", "Button 6"},
"Button 6", {"Common", "Button 7"},
"Button 7", {"Common", "Button 8"},
"Button 8", {"Common", "Button 9"},
"Button 9", {"SD", "Hi Lamp 1"},
"Top LED 1", {"SD", "Hi Lamp 2"},
"Top LED 2", {"SD", "Hi Lamp 3"},
"Top LED 3", {"SD", "Hi Lamp 4"},
"Top LED 4", {"SD", "Hi Lamp 5"},
"Top LED 5", {"SD", "Left Lamp 1"},
"Top LED 6", {"SD", "Left Lamp 2"},
"Top LED 7", {"SD", "Right Lamp 1"},
"Top LED 8", {"SD", "Right Lamp 2"},
"Top LED 9", {"HD", "Top LED 1"},
"Top LED 10", {"HD", "Top LED 2"},
"Top LED 11", {"HD", "Top LED 3"},
"Top LED 12", {"HD", "Top LED 4"},
"Top LED 13", {"HD", "Top LED 5"},
"Top LED 14", {"HD", "Top LED 6"},
"Top LED 15", {"HD", "Top LED 7"},
"Top LED 16", {"HD", "Top LED 8"},
"Top LED 17", {"HD", "Top LED 9"},
"Top LED 18", {"HD", "Top LED 10"},
"Top LED 19", {"HD", "Top LED 11"},
"Top LED 20", {"HD", "Top LED 12"},
"Top LED 21", {"HD", "Top LED 13"},
"Top LED 22", {"HD", "Top LED 14"},
"Top LED 23", {"HD", "Top LED 15"},
"Top LED 24", {"HD", "Top LED 16"},
"Top LED 25", {"HD", "Top LED 17"},
"Top LED 26", {"HD", "Top LED 18"},
"Top LED 27", {"HD", "Top LED 19"},
"Top LED 28", {"HD", "Top LED 20"},
"Top LED 29", {"HD", "Top LED 21"},
"Top LED 30", {"HD", "Top LED 22"},
"Top LED 31", {"HD", "Top LED 23"},
"Top LED 32", {"HD", "Top LED 24"},
"Top LED R", {"HD", "Top LED 25"},
"Top LED G", {"HD", "Top LED 26"},
"Top LED B", {"HD", "Top LED 27"},
"Hi Lamp 1", {"HD", "Top LED 28"},
"Hi Lamp 2", {"HD", "Top LED 29"},
"Hi Lamp 3", {"HD", "Top LED 30"},
"Hi Lamp 4", {"HD", "Top LED 31"},
"Hi Lamp 5", {"HD", "Top LED 32"},
"Left Lamp 1", {"HD/Pika", "Top LED R"},
"Left Lamp 2", {"HD/Pika", "Top LED G"},
"Right Lamp 1", {"HD/Pika", "Top LED B"},
"Right Lamp 2", {"HD/Pika", "Woofer LED R"},
"Woofer LED R", {"HD/Pika", "Woofer LED G"},
"Woofer LED G", {"HD/Pika", "Woofer LED B"},
"Woofer LED B" {"Pika", "Button 1 R"},
); {"Pika", "Button 1 G"},
{"Pika", "Button 1 B"},
{"Pika", "Button 2 R"},
{"Pika", "Button 2 G"},
{"Pika", "Button 2 B"},
{"Pika", "Button 3 R"},
{"Pika", "Button 3 G"},
{"Pika", "Button 3 B"},
{"Pika", "Button 4 R"},
{"Pika", "Button 4 G"},
{"Pika", "Button 4 B"},
{"Pika", "Button 5 R"},
{"Pika", "Button 5 G"},
{"Pika", "Button 5 B"},
{"Pika", "Button 6 R"},
{"Pika", "Button 6 G"},
{"Pika", "Button 6 B"},
{"Pika", "Button 7 R"},
{"Pika", "Button 7 G"},
{"Pika", "Button 7 B"},
{"Pika", "Button 8 R"},
{"Pika", "Button 8 G"},
{"Pika", "Button 8 B"},
{"Pika", "Button 9 R"},
{"Pika", "Button 9 G"},
{"Pika", "Button 9 B"},
{"Pika", "Red Pop-Kun R"},
{"Pika", "Red Pop-Kun G"},
{"Pika", "Red Pop-Kun B"},
{"Pika", "Blue Pop-Kun R"},
{"Pika", "Blue Pop-Kun G"},
{"Pika", "Blue Pop-Kun B"},
{"Pika", "IC Card R"},
{"Pika", "IC Card G"},
{"Pika", "IC Card B"},
});
} }
return lights; return lights;
+45 -9
View File
@@ -38,6 +38,15 @@ namespace games::popn {
Button7, Button7,
Button8, Button8,
Button9, Button9,
HiLamp1,
HiLamp2,
HiLamp3,
HiLamp4,
HiLamp5,
LeftLamp1,
LeftLamp2,
RightLamp1,
RightLamp2,
TopLED1, TopLED1,
TopLED2, TopLED2,
TopLED3, TopLED3,
@@ -73,18 +82,45 @@ namespace games::popn {
TopLED_R, TopLED_R,
TopLED_G, TopLED_G,
TopLED_B, TopLED_B,
HiLamp1,
HiLamp2,
HiLamp3,
HiLamp4,
HiLamp5,
LeftLamp1,
LeftLamp2,
RightLamp1,
RightLamp2,
WooferLED_R, WooferLED_R,
WooferLED_G, WooferLED_G,
WooferLED_B, WooferLED_B,
PikaButton1_R,
PikaButton1_G,
PikaButton1_B,
PikaButton2_R,
PikaButton2_G,
PikaButton2_B,
PikaButton3_R,
PikaButton3_G,
PikaButton3_B,
PikaButton4_R,
PikaButton4_G,
PikaButton4_B,
PikaButton5_R,
PikaButton5_G,
PikaButton5_B,
PikaButton6_R,
PikaButton6_G,
PikaButton6_B,
PikaButton7_R,
PikaButton7_G,
PikaButton7_B,
PikaButton8_R,
PikaButton8_G,
PikaButton8_B,
PikaButton9_R,
PikaButton9_G,
PikaButton9_B,
RedPopKun_R,
RedPopKun_G,
RedPopKun_B,
BluePopKun_R,
BluePopKun_G,
BluePopKun_B,
IC_Card_R,
IC_Card_G,
IC_Card_B,
} popn_lights_t; } popn_lights_t;
} }
+40
View File
@@ -32,6 +32,46 @@ namespace games::popn {
constexpr LONG FAKE_MONITOR_WIDTH = 1280; constexpr LONG FAKE_MONITOR_WIDTH = 1280;
constexpr LONG FAKE_MONITOR_HEIGHT = 800; constexpr LONG FAKE_MONITOR_HEIGHT = 800;
// pin 0 = buttons 1-3, size 9 (but it's actually triple that in reality - game passes in wrong length), values 0-0xFF
// pin 1 = left pop (red pop kun), size 16, values 0-0xFF
// pin 2 = right pop (blue pop kun), size 16, values 0-0xFF
// pin 3 = title, size 44, values 0-0xFF
// pin 4 = speaker, size 40, values 0-0xFF - U region only
// pin 5 = monitor, size 18, values 0-0x7F - U region only
// pin 6 = control panel, size 28, values 0-0x7F - U region only
// pin 7 = cabinet, size 22, values 0-0x7F - U region only
tapeledutils::tape_led TAPELED_MAPPING[POPN_TAPELED_TOTAL] = {
{
0, // these are button lights and handled specially
0,
0,
0,
"Invalid"
},
{
5, // 5*3; game reports 16 but make it 15
Lights::popn_lights_t::RedPopKun_R,
Lights::popn_lights_t::RedPopKun_G,
Lights::popn_lights_t::RedPopKun_B,
"Red Pop-Kun"
},
{
5, // 5*3; game reports 16 but make it 15
Lights::popn_lights_t::BluePopKun_R,
Lights::popn_lights_t::BluePopKun_G,
Lights::popn_lights_t::BluePopKun_B,
"Blue Pop-Kun"
},
{
14, // 14*3; game reports 44 but make it 42; these are lights above the banner
Lights::popn_lights_t::TopLED_R,
Lights::popn_lights_t::TopLED_G,
Lights::popn_lights_t::TopLED_B,
"Top"
},
};
static decltype(GetDisplayConfigBufferSizes) *GetDisplayConfigBufferSizes_orig = nullptr; static decltype(GetDisplayConfigBufferSizes) *GetDisplayConfigBufferSizes_orig = nullptr;
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr; static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr; static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
+6
View File
@@ -2,6 +2,7 @@
#include "avs/game.h" #include "avs/game.h"
#include "games/game.h" #include "games/game.h"
#include "util/tapeled.h"
namespace games::popn { namespace games::popn {
@@ -15,6 +16,11 @@ namespace games::popn {
} }
#if SPICE64
constexpr int POPN_TAPELED_TOTAL = 4;
extern tapeledutils::tape_led TAPELED_MAPPING[POPN_TAPELED_TOTAL];
#endif
extern bool SHOW_PIKA_MONITOR_WARNING; extern bool SHOW_PIKA_MONITOR_WARNING;
class POPNGame : public games::Game { class POPNGame : public games::Game {