how things go, inputly

-- @my name is literally dom how goes things
11 Replies
my name is literally dom
hey things go slowly but surely - it's starting to take shape, but ive been p busy lately. gonna keep at it this weekend tho and hopefully get a proper draft out
Perksey
PerkseyOP2mo ago
awesome stuff. lmk when you feel confident enough for me to find a date for the review.
my name is literally dom
the weekend of 24-26th should be good with me, but ill have something for you to take a first look at hopefully this weekend, if not then def next weekend I'm starting to approach something I like - there's a fair bit of stuff im leaving out rn bc I haven't finished it, but it does rely on this part:
Perksey
PerkseyOP2mo ago
nice! only thing i'm not a fan of is the normalizer API, the rest of it is a good API shape (obviously there's nits that are easier to review when it goes to PR but this isn't extreme). also the axis device registry, this likely shouldn't be static. I'd imagine a non-static version of this is what will end up in input context or a related API that can interact with an input context.
my name is literally dom
thanks for the feedback! ill incorporate that and mull things over from a composability perspective, how do you feel about this?
/// <summary>
/// Used to map an axis from one device to a remapped device
/// </summary>
public record struct AxisMapping(IAxisDevice SourceDevice, int SourceAxisIndex);

/// <summary>
/// Used to map an axis group from one device to a remapped device
/// </summary>>
public record struct AxisGroupMapping(IAxisDevice SourceDevice, int SourceGroupIndex);
/// <summary>
/// Used to map an axis from one device to a remapped device
/// </summary>
public record struct AxisMapping(IAxisDevice SourceDevice, int SourceAxisIndex);

/// <summary>
/// Used to map an axis group from one device to a remapped device
/// </summary>>
public record struct AxisGroupMapping(IAxisDevice SourceDevice, int SourceGroupIndex);
/// <summary>
/// An interface/base class for constructing virtual devices from existing devices
/// The lowest level of virtual device composability / the root layer of input remapping and input accessibility.
/// It is not terribly useful by itself.
///
/// Should be paired with other interfaces on a higher level, e.g. IGamepad, IKeyboard, IPointerDevice, etc,
/// and should be supplemented by actual implementations or higher level "Virtual Device" interfaces that make
/// this process more user-friendly
///
/// This is an abstract class so as to facilitate the creation of default implementations of this. The more think about it though,
/// the more torn I feel between making this an interface or a full non-abstract base class
/// </summary>
public abstract class VirtualDevice : IAxisDevice /*, IAxisInputHandler */
{
/// <summary>
/// The axes that are mapped to this virtual device.
/// This list must be identical in length to the number of axes in this virtual device.
/// If an axis is not mapped, that list element must be null.
/// </summary>
public abstract IReadOnlyList<AxisMapping?> AxisMappings { get; }

/// <summary>
/// The axis groups that are mapped to this virtual device.
/// This list must be identical in length to the number of axis groups in this virtual device.
///
/// If an axis group is not mapped externally, that list element must be null, even if all axes are individually mapped in the group.
/// OR
/// If an axis group is not mapped externally, but all of its axes are individually mapped, the AxisGroupMapping would be defined as (this, groupIndex).
///
/// Not sure which is a better approach.
/// </summary>
public abstract IReadOnlyList<AxisGroupMapping?> AxisGroupMappings { get; }

/// <summary>
/// An interface/base class for constructing virtual devices from existing devices
/// The lowest level of virtual device composability / the root layer of input remapping and input accessibility.
/// It is not terribly useful by itself.
///
/// Should be paired with other interfaces on a higher level, e.g. IGamepad, IKeyboard, IPointerDevice, etc,
/// and should be supplemented by actual implementations or higher level "Virtual Device" interfaces that make
/// this process more user-friendly
///
/// This is an abstract class so as to facilitate the creation of default implementations of this. The more think about it though,
/// the more torn I feel between making this an interface or a full non-abstract base class
/// </summary>
public abstract class VirtualDevice : IAxisDevice /*, IAxisInputHandler */
{
/// <summary>
/// The axes that are mapped to this virtual device.
/// This list must be identical in length to the number of axes in this virtual device.
/// If an axis is not mapped, that list element must be null.
/// </summary>
public abstract IReadOnlyList<AxisMapping?> AxisMappings { get; }

/// <summary>
/// The axis groups that are mapped to this virtual device.
/// This list must be identical in length to the number of axis groups in this virtual device.
///
/// If an axis group is not mapped externally, that list element must be null, even if all axes are individually mapped in the group.
/// OR
/// If an axis group is not mapped externally, but all of its axes are individually mapped, the AxisGroupMapping would be defined as (this, groupIndex).
///
/// Not sure which is a better approach.
/// </summary>
public abstract IReadOnlyList<AxisGroupMapping?> AxisGroupMappings { get; }

/// <summary>
/// Applies an axis group from the source device to this virtual device.
/// Populates the relevant axis mappings as well.
/// </summary>
public void SetAxisGroup(IAxisDevice sourceDevice, int sourceGroupIndex, int targetGroupIndex);

/// <summary>
/// Applies the specified axis from the source device to this virtual device
/// </summary>
public void SetAxis(IAxisDevice sourceDevice, int sourceAxisIndex, int targetAxisIndex);

/// <summary>
/// Clears the specified axis from this virtual device. Basicaly, <see cref="AxisMappings"/>[axisIndex] = null
/// </summary>
public void ClearAxis(int axisIndex);

/// <summary>
/// Clears the specified axis group from this virtual device. Basically, <see cref="AxisGroupMappings"/>[groupIndex] = null
/// Will also clear the individual axis mappings for the axes in the group if clearAxes is true
/// </summary>
/// <param name="groupIndex">The index of the axis group</param>
/// <param name="clearAxes">Whether the axis mappings for the axes in the group should also be cleared</param>
public void ClearAxisGroup(int groupIndex, bool clearAxes);

public bool HasAxis(int axisIndex) => AxisMappings[axisIndex] != null;

/// <summary>
/// Returns true if the device is fully mapped, with all required axes defined
/// </summary>
public virtual bool IsFullyMapped => AxisMappings.All(mapping => mapping != null);

/// <summary>
/// True if they are, false if 1 or more underlying devices have disconnected
/// </summary>
public bool AllPhysicalDevicesConnected { get; }

/// <summary>
/// True if the axis is mapped and the underlying device is connected
/// </summary>
public bool IsAxisAvailable(int axisIndex);

/// <summary>
/// Of course it is a virtual device
/// </summary>
bool IAxisDevice.IsVirtual => true;

/// <summary>
/// If true, calling <see cref="SetAxis"/> will set the desired axis, and if the input axis is already mapped to
/// another virtual axis, that other virtual axis must be cleared.
/// In other words, forces a 1:1 mapping of input axes to virtual axes.
/// </summary>
public bool ForceUniqueInputAxes { get; set; }

/// <summary>
/// Replaces all the used axes of the specified device with the matching axes of the new device
/// </summary>
/// <throws>InvalidOperationException if the devices are not the same type and no valid axis mappings are provided
/// to reconcile this. This compatibility is implementation-specific</throws>
public abstract void ReplaceDevice<T>(T oldDevice, T newDevice, IReadOnlyList<(int, int)>? axisMappings = null) where T : IAxisDevice;
}
/// <summary>
/// Applies an axis group from the source device to this virtual device.
/// Populates the relevant axis mappings as well.
/// </summary>
public void SetAxisGroup(IAxisDevice sourceDevice, int sourceGroupIndex, int targetGroupIndex);

/// <summary>
/// Applies the specified axis from the source device to this virtual device
/// </summary>
public void SetAxis(IAxisDevice sourceDevice, int sourceAxisIndex, int targetAxisIndex);

/// <summary>
/// Clears the specified axis from this virtual device. Basicaly, <see cref="AxisMappings"/>[axisIndex] = null
/// </summary>
public void ClearAxis(int axisIndex);

/// <summary>
/// Clears the specified axis group from this virtual device. Basically, <see cref="AxisGroupMappings"/>[groupIndex] = null
/// Will also clear the individual axis mappings for the axes in the group if clearAxes is true
/// </summary>
/// <param name="groupIndex">The index of the axis group</param>
/// <param name="clearAxes">Whether the axis mappings for the axes in the group should also be cleared</param>
public void ClearAxisGroup(int groupIndex, bool clearAxes);

public bool HasAxis(int axisIndex) => AxisMappings[axisIndex] != null;

/// <summary>
/// Returns true if the device is fully mapped, with all required axes defined
/// </summary>
public virtual bool IsFullyMapped => AxisMappings.All(mapping => mapping != null);

/// <summary>
/// True if they are, false if 1 or more underlying devices have disconnected
/// </summary>
public bool AllPhysicalDevicesConnected { get; }

/// <summary>
/// True if the axis is mapped and the underlying device is connected
/// </summary>
public bool IsAxisAvailable(int axisIndex);

/// <summary>
/// Of course it is a virtual device
/// </summary>
bool IAxisDevice.IsVirtual => true;

/// <summary>
/// If true, calling <see cref="SetAxis"/> will set the desired axis, and if the input axis is already mapped to
/// another virtual axis, that other virtual axis must be cleared.
/// In other words, forces a 1:1 mapping of input axes to virtual axes.
/// </summary>
public bool ForceUniqueInputAxes { get; set; }

/// <summary>
/// Replaces all the used axes of the specified device with the matching axes of the new device
/// </summary>
/// <throws>InvalidOperationException if the devices are not the same type and no valid axis mappings are provided
/// to reconcile this. This compatibility is implementation-specific</throws>
public abstract void ReplaceDevice<T>(T oldDevice, T newDevice, IReadOnlyList<(int, int)>? axisMappings = null) where T : IAxisDevice;
}
my name is literally dom
@Perksey Here's my latest "complete" version - not quite in Proposal Format, but I think all of the information is there for Ground Zero Axis Control
Perksey
PerkseyOP2mo ago
Oh god sorry meant to look at this today I’ll try and look at this once I get home but I might not be able to until the morning (afternoon) haven't seen anything I dislike, but do want to try and make it slightly more lean (mainly wrt the extensions). we should hop in a call at some point. e.g. virtual vs non-virtual devices (although I do see your intention, and maybe this should be its own extension proposal mainly so the approval of the axis concepts as a whole - for which I do approve of this proposal's interpretation - is not contigent on the approval of some of the more cooler-but-involved use cases that we get from approving such an API), and an audit of some of the extension methods, etc - all of which is probably best done in a PR format some similar comments from last time still apply but again these are probably best done at the PR stage so they're more trackable and individually discussable
my name is literally dom
ok sounds good! so do you suggest removing/separating the "remapping" and "extension method" sections? also im down to chat at some point tomorrow if youre around - if not then next weekend
Perksey
PerkseyOP2mo ago
remapping definitely, the extension methods may be fine we just need to make sure every API is well-understood and well-rationalised. there are a lot of APIs there is my main worry, which isn't necessarily a problem but could cause a headache for ongoing maintenance
my name is literally dom
yes sir 🫡 most of the extension api are basically just overloads/utility methods that should in theory be write-once-and-forget . im down to trim the fat to its necessary parts, but i think at least some of the others would necessarily follow

Did you find this page helpful?