Update to spice2x-25-04-25 (pre-apply)

> broken commit
This commit is contained in:
[ ]
2025-05-07 00:25:31 +09:00
parent 04dee88276
commit c94de456b5
543 changed files with 78491 additions and 91698 deletions
+287 -121
View File
@@ -17,6 +17,8 @@ std::vector<Button> GameAPI::Buttons::getButtons(Game *game) {
return Config::getInstance().getButtons(game);
}
static Buttons::State getMidiV2ButtonState(float last_on_time, float last_off_time);
std::vector<Button> GameAPI::Buttons::sortButtons(
const std::vector<Button> &buttons,
const std::vector<std::string> &button_names,
@@ -161,14 +163,17 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
break;
}
case BAT_NEGATIVE:
case BAT_POSITIVE: {
case BAT_POSITIVE:
case BAT_ANY: {
auto value_states = &hid->value_states;
if (vKey < value_states->size()) {
auto value = value_states->at(vKey);
if (current_button->getAnalogType() == BAT_POSITIVE) {
state = value > 0.6f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
} else {
} else if (current_button->getAnalogType() == BAT_NEGATIVE) {
state = value < 0.4f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
} else {
state = value > 0.01f ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
}
} else {
state = BUTTON_NOT_PRESSED;
@@ -216,50 +221,135 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
auto midi = device->midiInfo;
switch (bat) {
case BAT_NONE: {
if (vKey < 16 * 128) {
if (rawinput::get_midi_algorithm() == rawinput::MidiNoteAlgorithm::LEGACY) {
// spicetools legacy midi logic: use event log
//
// drums send NOTE_ON and NOTE_OFF in rapid succession, before game engine has a chance
// to poll for it - to address this, keep a counter (states_events array) and the last
// state (states array), incrementing the states_events on rising edges (NOTE_ON)
// and popping events off the queue every time it's checked.
//
// if the same drum pad is mapped to multiple buttons, multiple issues arise:
// 1. we run through this logic for each button, which consumes an event every time;
// therefore, the first button may see the ON event, but subsequent mappings may
// completely miss it as it already has been drained
// 2. it is impossible to implement velocity threshold with this logic since the
// velocity is a per-note value that goes away as soon as NOTE_OFF is detected
if (vKey < midi->states_events.size()) {
// check for event
auto midi_event = midi->states_events[vKey];
if (midi_event) {
// check for event
auto midi_event = midi->states_events[vKey];
if (midi_event) {
// choose state based on event
state = (midi_event % 2) ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
// choose state based on event
state = (midi_event % 2) ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
// update event
if (!midi->states[vKey] || midi_event > 1) {
midi->states_events[vKey]--;
}
} else {
state = BUTTON_NOT_PRESSED;
}
}
} else {
// spice2x midi logic (new!)
//
// for every MIDI NOTE ON message, latch the "on" for a certain time, even if NOTE
// OFF message is seen immediately afterwards.
//
// each ON event is held long enough for the game's input poll to see it (e.g., gitadora
// polls every 16ms or so, rawinput holds it for 20ms by default)
//
// this is much simpler and does not have the issues mentioned above for the legacy
// logic, however the downside is that there is a risk of coalescing rapid inputs into
// one.
//
// that being said:
// * default value of 20ms should be reasonable; humans can't realistically hit the
// same note faster than this; in fact it's likely to be a misfire
// * we can tweak it per-game if needed to suit the game's polling period (in the
// future)
// * as a last resort the user can always override it via the option (MidiNoteSustain)
if (vKey < midi->v2_last_on_time.size()) {
// update event
if (!midi->states[vKey] || midi_event > 1)
midi->states_events[vKey]--;
// take the velocity threshold from first button binding we encounter here
// this hardware key may be mapped to multiple bindings, but the UI should keep them
// the same value, as only one threshold value can be set per MIDI key
// (otherwise it makes the sustain logic too complicated)
const auto sw_threshold = current_button->getVelocityThreshold();
if (0 < sw_threshold && !midi->v2_velocity_threshold_set_on_device[vKey]) {
midi->v2_velocity_threshold_set_on_device[vKey] = true;
midi->v2_velocity_threshold[vKey] = sw_threshold;
}
} else
state = getMidiV2ButtonState(
midi->v2_last_on_time[vKey],
midi->v2_last_off_time[vKey]);
} else {
state = BUTTON_NOT_PRESSED;
}
}
break;
}
case BAT_MIDI_CTRL_PRECISION: {
if (vKey < 16 * 32)
state = midi->controls_precision[vKey] > 0 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
else
if (vKey < midi->controls_precision.size()) {
if (rawinput::get_midi_algorithm() == rawinput::MidiNoteAlgorithm::LEGACY) {
state = midi->controls_precision[vKey] > 0 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
} else {
// not using getVelocityHelper here to avoid locking and other checks
const auto v = device->midiInfo->controls_precision[vKey];
// velocity threshold ranges from [0, 127], so do some math for double precision
const auto threshold = (current_button->getVelocityThreshold() << 7u) | 0x7f;
state = (threshold < v) ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
}
} else {
state = BUTTON_NOT_PRESSED;
}
break;
}
case BAT_MIDI_CTRL_SINGLE: {
if (vKey < 16 * 44)
state = midi->controls_single[vKey] > 0 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
else
if (vKey < midi->controls_single.size()) {
if (rawinput::get_midi_algorithm() == rawinput::MidiNoteAlgorithm::LEGACY) {
state = midi->controls_single[vKey] > 0 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
} else {
// not using getVelocityHelper here to avoid locking and other checks
const auto v = device->midiInfo->controls_single[vKey];
state = (current_button->getVelocityThreshold() < v) ?
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
}
} else {
state = BUTTON_NOT_PRESSED;
}
break;
}
case BAT_MIDI_CTRL_ONOFF: {
if (vKey < 16 * 6)
state = midi->controls_onoff[vKey] ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
else
if (vKey < midi->controls_onoff.size()) {
if (rawinput::get_midi_algorithm() == rawinput::MidiNoteAlgorithm::LEGACY) {
state = midi->controls_onoff[vKey] ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
} else {
state = getMidiV2ButtonState(
midi->v2_controls_onoff_last_on_time[vKey],
midi->v2_controls_onoff_last_off_time[vKey]);
}
} else {
state = BUTTON_NOT_PRESSED;
}
break;
}
case BAT_MIDI_PITCH_DOWN:
state = midi->pitch_bend < 0x2000 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
if (vKey < midi->pitch_bend.size()) {
state = midi->pitch_bend[vKey] < 0 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
} else {
state = BUTTON_NOT_PRESSED;
}
break;
case BAT_MIDI_PITCH_UP:
state = midi->pitch_bend > 0x2000 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
if (vKey < midi->pitch_bend.size()) {
state = midi->pitch_bend[vKey] > 0 ? BUTTON_PRESSED : BUTTON_NOT_PRESSED;
} else {
state = BUTTON_NOT_PRESSED;
}
break;
default: {
state = BUTTON_NOT_PRESSED;
@@ -372,24 +462,64 @@ static float getVelocityHelper(rawinput::RawInputManager *manager, Button &butto
device->mutex->lock();
// determine velocity based on device type
switch (device->type) {
case rawinput::MIDI: {
if (device->type == rawinput::MIDI) {
switch (button.getAnalogType()) {
case ButtonAnalogType::BAT_MIDI_CTRL_PRECISION:
if (vKey < device->midiInfo->controls_precision.size()) {
velocity = device->midiInfo->controls_precision[vKey] / 16383.f;
} else {
velocity = 0.f;
}
break;
// read control
if (vKey < 16 * 128) {
velocity = (float) device->midiInfo->velocity[vKey] / 127.f;
} else {
velocity = 0.f;
}
case ButtonAnalogType::BAT_MIDI_CTRL_SINGLE:
if (vKey < device->midiInfo->controls_single.size()) {
velocity = device->midiInfo->controls_single[vKey] / 127.f;
} else {
velocity = 0.f;
}
break;
// invert
if (button.getInvert()) {
velocity = 1.f - velocity;
}
break;
case ButtonAnalogType::BAT_MIDI_CTRL_ONOFF:
if (vKey < device->midiInfo->controls_onoff.size()) {
velocity = device->midiInfo->controls_onoff[vKey] ? 1.f : 0.f;
} else {
velocity = 0.f;
}
break;
case ButtonAnalogType::BAT_MIDI_PITCH_DOWN:
if (vKey < device->midiInfo->pitch_bend.size()) {
velocity = device->midiInfo->pitch_bend[vKey] < 0 ? 1.f : 0.f;
} else {
velocity = 0.f;
}
break;
case ButtonAnalogType::BAT_MIDI_PITCH_UP:
if (vKey < device->midiInfo->pitch_bend.size()) {
// pitch range is [-8192, 8191]
velocity = (device->midiInfo->pitch_bend[vKey]) > 0 ? 1.f : 0.f;
} else {
velocity = 0.f;
}
break;
case ButtonAnalogType::BAT_NONE:
default:
// velocity sensitive
if (vKey < device->midiInfo->velocity.size()) {
velocity = (float) device->midiInfo->velocity[vKey] / 127.f;
} else {
velocity = 0.f;
}
break;
}
// invert
if (button.getInvert()) {
velocity = 1.f - velocity;
}
default:
break;
}
// unlock device
@@ -490,29 +620,82 @@ float GameAPI::Analogs::getState(rawinput::Device *device, Analog &analog) {
value = device->hidInfo->value_states[index];
}
// smoothing/sensitivity
if (analog.getSmoothing() || analog.isSensitivitySet()) {
float rads = value * (float) M_TAU;
// deadzone
if (analog.isDeadzoneSet()) {
value = analog.applyDeadzone(value);
}
// smoothing
if (analog.getSmoothing()) {
if (analog.isRelativeMode()) {
float relative_delta = value - 0.5f;
// built-in scaling to make values reasonable
relative_delta /= 80.f;
// preserve direction
if (rads >= M_TAU) {
rads -= 0.0001f;
// integer multiplier/divisor
const auto mult = analog.getMultiplier();
if (mult < -1) {
relative_delta /= -mult;
} else if (1 < mult) {
relative_delta *= mult;
}
// sensitivity (ranges from 0.0 to 4.0)
if (analog.isSensitivitySet()) {
relative_delta *= analog.getSensitivity();
}
// translate relative movement to absolute value
value = analog.getAbsoluteValue(relative_delta);
} else {
// integer multiplier
value = analog.applyMultiplier(value);
// smoothing/sensitivity
if (analog.getSmoothing() || analog.isSensitivitySet()) {
float rads = value * (float) M_TAU;
// smoothing
if (analog.getSmoothing()) {
// preserve direction
if (rads >= M_TAU) {
rads -= 0.0001f;
}
// calculate angle
rads = analog.getSmoothedValue(rads);
}
// calculate angle
rads = analog.getSmoothedValue(rads);
// sensitivity
if (analog.isSensitivitySet()) {
rads = analog.applyAngularSensitivity(rads);
}
// apply to value
value = rads * (float) M_1_TAU;
}
}
// delay
if (0 < analog.getDelayBufferDepth()) {
auto& queue = analog.getDelayBuffer();
// ensure the queue isn't too long; drop old values
while (analog.getDelayBufferDepth() <= (int)queue.size()) {
queue.pop();
}
// sensitivity
if (analog.isSensitivitySet()) {
rads = analog.applyAngularSensitivity(rads);
}
// always push new value
queue.push(value);
// apply to value
value = rads * (float) M_1_TAU;
// get a new value to return
if ((int)queue.size() < analog.getDelayBufferDepth()) {
// not enough in the queue, stall for now, shouldn't happen often
value = analog.getLastState();
} else {
value = queue.front();
queue.pop();
}
}
break;
@@ -524,6 +707,7 @@ float GameAPI::Analogs::getState(rawinput::Device *device, Analog &analog) {
auto prec_count = (int) midi->controls_precision.size();
auto single_count = (int) midi->controls_single.size();
auto onoff_count = (int) midi->controls_onoff.size();
auto pitch_count = (int) midi->pitch_bend.size();
// decide on value
if (index < prec_count)
@@ -532,81 +716,19 @@ float GameAPI::Analogs::getState(rawinput::Device *device, Analog &analog) {
value = midi->controls_single[index - prec_count] / 127.f;
else if (index < prec_count + single_count + onoff_count)
value = midi->controls_onoff[index - prec_count - single_count] ? 1.f : 0.f;
else if (index == prec_count + single_count + onoff_count)
value = midi->pitch_bend / 16383.f;
else if (index < prec_count + single_count + onoff_count + pitch_count)
value = (midi->pitch_bend[index - prec_count - single_count - onoff_count] + 0x2000) / 16383.f;
// invert value
if (inverted) {
value = 1.f - value;
}
}
default:
break;
}
// deadzone logic
switch (device->type) {
case rawinput::HID:
case rawinput::MIDI: {
// check if set
// deadzone
if (analog.isDeadzoneSet()) {
// check sign
auto deadzone = analog.getDeadzone();
if (deadzone > 0) {
// calculate values
auto delta = value - 0.5f;
auto dtlen = 1.f - deadzone;
// check mirror
if (analog.getDeadzoneMirror()) {
// deadzone on the edges
if (dtlen != 0.f) {
value = std::max(0.f, std::min(1.f, 0.5f + (delta / dtlen)));
} else {
value = 0.5f;
}
} else {
// deadzone around the middle
auto limit = deadzone * 0.5f;
if (dtlen != 0.f) {
if (delta > limit) {
value = std::min(1.f, 0.5f + std::max(0.f, (delta - limit) / dtlen));
} else if (delta < -limit) {
value = std::max(0.f, 0.5f + std::min(0.f, (delta + limit) / dtlen));
} else {
value = 0.5f;
}
} else {
value = 0.5f;
}
}
} else if (deadzone < 0) {
// invert for mirror
if (analog.getDeadzoneMirror()) {
value = 1.f - value;
}
// deadzone from minimum value
if (deadzone > -1 && value > -deadzone) {
value = std::min(1.f, (value + deadzone) / (1.f + deadzone));
} else {
value = 0.f;
}
// revert value for mirror
if (analog.getDeadzoneMirror()) {
value = 1.f - value;
}
}
value = analog.applyDeadzone(value);
}
break;
}
default:
break;
@@ -761,6 +883,7 @@ void GameAPI::Lights::writeLight(rawinput::Device *device, int index, float valu
if (index < rawinput::SextetDevice::LIGHT_COUNT) {
device->sextetInfo->light_state[index] = value > 0;
device->sextetInfo->push_light_state();
device->output_pending = true;
} else {
log_warning("api", "invalid sextet light index: {}", index);
}
@@ -769,11 +892,30 @@ void GameAPI::Lights::writeLight(rawinput::Device *device, int index, float valu
case rawinput::PIUIO_DEVICE: {
if (index < rawinput::PIUIO::PIUIO_MAX_NUM_OF_LIGHTS) {
device->piuioDev->SetLight(index, value > 0);
device->output_pending = true;
} else {
log_warning("api", "invalid piuio light index: {}", index);
}
break;
}
case rawinput::SMX_STAGE: {
if (index < rawinput::SmxStageDevice::TOTAL_LIGHT_COUNT) {
device->smxstageInfo->SetLightByIndex(index, static_cast<uint8_t>(value*255.f));
device->output_pending = true;
} else {
log_warning("api", "invalid smx stage light index: {}", index);
}
break;
}
case rawinput::SMX_DEDICAB: {
if (index < rawinput::SmxDedicabDevice::LIGHTS_COUNT) {
device->smxdedicabInfo->SetLightByIndex(index, static_cast<uint8_t>(value * 255.f));
device->output_pending = true;
} else {
log_warning("api", "invalid SMX dedicab light index: {}", index);
}
break;
}
default:
break;
}
@@ -922,3 +1064,27 @@ void GameAPI::Options::sortOptions(std::vector<Option> &options, const std::vect
options = std::move(sorted);
}
static Buttons::State getMidiV2ButtonState(float on, float off) {
if (on == 0.0) {
return Buttons::State::BUTTON_NOT_PRESSED;
} else if (off < on) {
// if OFF was not observed strictly after ON, we can confidently say that the note
// remains ON; in case of a tie (rarely in v2, all the time in v2_drum), prefer to keep note
// off since that's better than a note stuck on
return Buttons::State::BUTTON_PRESSED;
} else {
// otherwise, this is an ON-OFF sequence
// check for time the most recent ON message
//
// if recent, consider the button to be on - even if there were OFF messages following it
// this is needed to detect things like MIDI drums which send a quick ON-OFF sequence
// between the game's polling period
const auto now = get_performance_milliseconds();
if ((now - on) < (double)rawinput::MIDI_NOTE_SUSTAIN) {
return Buttons::State::BUTTON_PRESSED;
} else {
return Buttons::State::BUTTON_NOT_PRESSED;
}
}
}