vkdoom_m/src/common/engine/m_haptics.cpp
2025-08-24 03:02:06 -04:00

381 lines
10 KiB
C++

/*
** m_haptics.cpp
**
** Haptic feedback implementation
**
**---------------------------------------------------------------------------
**
** Copyright 2025 Marcus Minhorst
** Copyright 2025 ZDoom + GZDoom teams, and contributors
**
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program. If not, see http://www.gnu.org/licenses/
**
**---------------------------------------------------------------------------
**
*/
// HEADER FILES ------------------------------------------------------------
#include <math.h>
#include "c_cvars.h"
#include "doomstat.h"
#include "m_haptics.h"
#include "name.h"
#include "printf.h"
#include "s_soundinternal.h"
#include "tarray.h"
#include "zstring.h"
// MACROS ------------------------------------------------------------------
#ifndef MAX_TRY_DEPTH
#define MAX_TRY_DEPTH 8
#endif
// TYPES -------------------------------------------------------------------
// EXTERNAL FUNCTION PROTOTYPES --------------------------------------------
// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------
// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------
// EXTERNAL DATA DECLARATIONS ----------------------------------------------
extern bool AppActive;
// PRIVATE DATA DEFINITIONS ------------------------------------------------
struct {
int tic = gametic; // last tic processed
bool dirty = false; // do we need to do something next tick ?
bool enabled = true; // do we need to do anything ever ?
bool active = true; // is the game currently not paused ?
struct Haptics current = {0,0,0,0,0}; // current state of the controller
TMap<FName, struct Haptics> channels; // active rumbles (that will be mixed)
} Haptics;
TMap<FName, struct Haptics> RumbleDefinition = {};
TMap<FName, FName> RumbleMapping = {};
TMap<FName, FName> RumbleAlias = {};
// fallback names. these exist in base sndinfo
const FName HapticIntense = "INTENSE",
HapticHeavy = "HEAVY",
HapticMedium = "MEDIUM",
HapticLight = "LIGHT",
HapticSubtle = "SUBTLE";
// PUBLIC DATA DEFINITIONS -------------------------------------------------
CVARD(Bool, haptics_debug, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG, "print diagnostics for haptic feedback");
// CODE --------------------------------------------------------------------
//==========================================================================
//
// Joy_GetMapping
//
// Takes a sound name, and find its corresponding rumble identifier.
//
//==========================================================================
const FName * Joy_GetMapping(const FName identifier)
{
const FName * mapping = RumbleMapping.CheckKey(identifier);
if (!mapping && identifier != "")
{
Printf(DMSG_WARNING, "Unknown rumble mapping '%s'\n", identifier.GetChars());
}
return mapping;
}
//==========================================================================
//
// Joy_GetRumble
//
// Takes a rumble identifier, and returns its haptics definition.
//
//==========================================================================
const struct Haptics * Joy_GetRumble(FName identifier)
{
struct Haptics * rumble = RumbleDefinition.CheckKey(identifier);
if (!rumble) {
Printf(DMSG_ERROR, TEXTCOLOR_RED "Rumble mapping not found! '%s'\n", identifier.GetChars());
return nullptr;
}
return rumble;
}
//==========================================================================
//
// Joy_AddRumbleType
//
// Ties a rumble identifier to a haptics definition.
//
//==========================================================================
void Joy_AddRumbleType(const FName identifier, const struct Haptics data)
{
RumbleDefinition.Insert(identifier, data);
}
//==========================================================================
//
// Joy_AddRumbleAlias
//
// Adds an alternative rumble identifier for a haptics definition.
// Joy_ReadyRumbleMapping must be called after adding any aliases.
//
//==========================================================================
void Joy_AddRumbleAlias(const FName alias, const FName actual)
{
RumbleAlias.Insert(alias, actual);
}
//==========================================================================
//
// Joy_MapRumbleType
//
// Ties a sound to a rumble identifier.
//
//==========================================================================
void Joy_MapRumbleType(const FName sound, const FName identifier)
{
RumbleMapping.Insert(sound, identifier);
}
//==========================================================================
//
// Joy_ResetRumbleMapping
//
// Resets entire rumble state
//
//==========================================================================
void Joy_ResetRumbleMapping()
{
RumbleMapping.Clear();
RumbleAlias.Clear();
RumbleDefinition.Clear();
Haptics.channels.Clear();
Haptics.current.high_frequency = Haptics.current.low_frequency =
Haptics.current.left_trigger = Haptics.current.right_trigger =
Haptics.current.ticks = 0;
I_Rumble(0, 0, 0, 0);
}
//==========================================================================
//
// Joy_ReadyRumbleMapping
//
// Resets entire rumble state
//
//==========================================================================
void Joy_ReadyRumbleMapping()
{
TArray<FName> found;
TMapIterator<FName, FName> it(RumbleAlias);
TMap<FName, FName>::Pair* pair;
while (RumbleAlias.CountUsed())
{
while (it.NextPair(pair))
{
auto predefined = RumbleDefinition.CheckKey(pair->Key);
if (predefined)
{
Printf(DMSG_ERROR, TEXTCOLOR_RED "Rumble alias trying to redefine mapping! '%s'\n", pair->Key.GetChars());
continue;
}
auto mapping = RumbleDefinition.CheckKey(pair->Value);
if (!mapping) continue;
found.AddUnique(pair->Key);
RumbleDefinition.Insert(pair->Key, *mapping);
}
it.Reset();
if (found.Size() == 0)
{
FString list = "[";
while (it.NextPair(pair))
list.AppendFormat(" '%s'->'%s'", pair->Key.GetChars(), pair->Value.GetChars());
Printf(DMSG_ERROR, TEXTCOLOR_RED "Circular rumble alias found! (%d) %s ]\n", RumbleAlias.CountUsed(), list.GetChars());
break;
}
while (found.size() > 0) {
RumbleAlias.Remove(found.Last());
found.Pop();
}
}
}
//==========================================================================
//
// Joy_RumbleTick
//
// Called once per gametic. Manages rumble state.
//
//==========================================================================
void Joy_RumbleTick() {
if (!Haptics.enabled) return;
// pause detection
if (AppActive != Haptics.active)
{
Haptics.active = AppActive;
if (Haptics.active && Haptics.current.ticks != 0)
{
I_Rumble(
Haptics.current.high_frequency,
Haptics.current.low_frequency,
Haptics.current.left_trigger,
Haptics.current.right_trigger
);
}
else
{
I_Rumble(0, 0, 0, 0);
}
}
if (Haptics.tic >= gametic) return;
Haptics.tic = gametic;
// new value OR time elapsed
Haptics.dirty |= Haptics.current.ticks != 0 && Haptics.current.ticks < Haptics.tic;
if (!Haptics.dirty) return;
// init
Haptics.current.ticks = 0;
Haptics.current.high_frequency = 0;
Haptics.current.low_frequency = 0;
Haptics.current.left_trigger = 0;
Haptics.current.right_trigger = 0;
TMapIterator<FName, struct Haptics> it(Haptics.channels);
TMap<FName, struct Haptics>::Pair* pair;
TArray<FName> stale;
// remove old rumble data
while (it.NextPair(pair))
{
if (pair->Value.ticks < Haptics.tic)
{
stale.Push(pair->Key);
}
}
for (auto key: stale)
{
Haptics.channels.Remove(key);
}
it.Reset();
while (it.NextPair(pair))
{
// grab upcoming event time
Haptics.current.ticks = Haptics.current.ticks == 0
? pair->Value.ticks
: std::min(Haptics.current.ticks, pair->Value.ticks);
// simple intensity mixing
Haptics.current.high_frequency += pair->Value.high_frequency;
Haptics.current.low_frequency += pair->Value.low_frequency;
Haptics.current.left_trigger += pair->Value.left_trigger;
Haptics.current.right_trigger += pair->Value.right_trigger;
}
// should this be clamped to [0,1]? Maybe a controller api will support "hdr" haptics lol
// Haptics.current.high_frequency = std::min(std::max(0.0, Haptics.current.high_frequency), 1.0);
// Haptics.current.low_frequency = std::min(std::max(0.0, Haptics.current.low_frequency), 1.0);
// Haptics.current.left_trigger = std::min(std::max(0.0, Haptics.current.left_trigger), 1.0);
// Haptics.current.right_trigger = std::min(std::max(0.0, Haptics.current.right_trigger), 1.0);
I_Rumble(
Haptics.current.high_frequency,
Haptics.current.low_frequency,
Haptics.current.left_trigger,
Haptics.current.right_trigger
);
Haptics.dirty = false;
}
//==========================================================================
//
// Joy_Rumble
//
// Requests haptic feedback. Attenuation of >=1 results in no-op.
//
//==========================================================================
void Joy_Rumble(const FName source, const struct Haptics data, double attenuation)
{
if (!Haptics.enabled) return;
if (data.ticks <= 0) return;
if (attenuation >= 1) return;
float strength = 1 - (attenuation < 0? 0: attenuation);
if (haptics_debug)
Printf("r %s * %g\n", source.GetChars(), strength);
// this will overwrite stuff from same source mapping (weapons/pistol not W_BULLET)
Haptics.channels.Insert(source, {
Haptics.tic + data.ticks + 1,
data.high_frequency * strength,
data.low_frequency * strength,
data.left_trigger * strength,
data.right_trigger * strength,
});
Haptics.dirty = true;
}
//==========================================================================
//
// Joy_Rumble
//
// Requests haptic feedback. Attenuation of >=1 results in no-op.
//
//==========================================================================
void Joy_Rumble(const FName identifier, double attenuation)
{
const FName * mapping = Joy_GetMapping(identifier);
if (mapping == nullptr) return;
const struct Haptics * rumble = Joy_GetRumble(*mapping);
if (rumble == nullptr) return;
Joy_Rumble(identifier, * rumble, attenuation);
}