bsnes/higan/sfc/controller/super-scope/super-scope.cpp

127 lines
3.5 KiB
C++

//The Super Scope is a light-gun: it detects the CRT beam cannon position,
//and latches the counters by toggling iobit. This only works on controller
//port 2, as iobit there is connected to the PPU H/V counter latch.
//(PIO $4201.d7)
//It is obviously not possible to perfectly simulate an IR light detecting
//a CRT beam cannon, hence this class will read the PPU raster counters.
//A Super Scope can still technically be used in port 1, however it would
//require manual polling of PIO ($4201.d6) to determine when iobit was written.
//Note that no commercial game ever utilizes a Super Scope in port 1.
SuperScope::SuperScope(bool port) : Controller(port) {
create(Controller::Enter, 21'477'272);
sprite = Emulator::video.createSprite(32, 32);
sprite->setPixels(Resource::Sprite::CrosshairGreen);
latched = 0;
counter = 0;
//center cursor onscreen
x = 256 / 2;
y = 240 / 2;
trigger = false;
cursor = false;
turbo = false;
pause = false;
offscreen = false;
oldturbo = false;
triggerlock = false;
pauselock = false;
prev = 0;
}
SuperScope::~SuperScope() {
Emulator::video.removeSprite(sprite);
}
auto SuperScope::main() -> void {
uint next = cpu.vcounter() * 1364 + cpu.hcounter();
if(!offscreen) {
uint target = y * 1364 + (x + 24) * 4;
if(next >= target && prev < target) {
//CRT raster detected, toggle iobit to latch counters
iobit(0);
iobit(1);
}
}
if(next < prev) {
//Vcounter wrapped back to zero; update cursor coordinates for start of new frame
int nx = interface->inputPoll(port, ID::Device::SuperScope, X);
int ny = interface->inputPoll(port, ID::Device::SuperScope, Y);
nx += x;
ny += y;
x = max(-16, min(256 + 16, nx));
y = max(-16, min(240 + 16, ny));
offscreen = (x < 0 || y < 0 || x >= 256 || y >= ppu.vdisp());
sprite->setPosition(x * 2 - 16, y * 2 - 16);
sprite->setVisible(true);
}
prev = next;
step(2);
}
auto SuperScope::data() -> uint2 {
if(counter >= 8) return 1;
if(counter == 0) {
//turbo is a switch; toggle is edge sensitive
bool newturbo = interface->inputPoll(port, ID::Device::SuperScope, Turbo);
if(newturbo && !oldturbo) {
turbo = !turbo; //toggle state
sprite->setPixels(turbo ? Resource::Sprite::CrosshairRed : Resource::Sprite::CrosshairGreen);
}
oldturbo = newturbo;
//trigger is a button
//if turbo is active, trigger is level sensitive; otherwise, it is edge sensitive
trigger = false;
bool newtrigger = interface->inputPoll(port, ID::Device::SuperScope, Trigger);
if(newtrigger && (turbo || !triggerlock)) {
trigger = true;
triggerlock = true;
} else if(!newtrigger) {
triggerlock = false;
}
//cursor is a button; it is always level sensitive
cursor = interface->inputPoll(port, ID::Device::SuperScope, Cursor);
//pause is a button; it is always edge sensitive
pause = false;
bool newpause = interface->inputPoll(port, ID::Device::SuperScope, Pause);
if(newpause && !pauselock) {
pause = true;
pauselock = true;
} else if(!newpause) {
pauselock = false;
}
offscreen = (x < 0 || y < 0 || x >= 256 || y >= ppu.vdisp());
}
switch(counter++) {
case 0: return offscreen ? 0 : trigger;
case 1: return cursor;
case 2: return turbo;
case 3: return pause;
case 4: return 0;
case 5: return 0;
case 6: return offscreen;
case 7: return 0; //noise (1 = yes)
}
}
auto SuperScope::latch(bool data) -> void {
if(latched == data) return;
latched = data;
counter = 0;
}