SextetStream input and lights drivers.
This commit is contained in:
+4
-2
@@ -237,10 +237,12 @@ Threads = arch/Threads/Threads.h
|
||||
# ===================================
|
||||
# (Potentially) multiplatform drivers
|
||||
# ===================================
|
||||
InputHandler += arch/InputHandler/InputHandler_MonkeyKeyboard.cpp arch/InputHandler/InputHandler_MonkeyKeyboard.h
|
||||
InputHandler += arch/InputHandler/InputHandler_MonkeyKeyboard.cpp arch/InputHandler/InputHandler_MonkeyKeyboard.h \
|
||||
arch/InputHandler/InputHandler_SextetStream.cpp arch/InputHandler/InputHandler_SextetStream.h
|
||||
|
||||
Lights += arch/Lights/LightsDriver_Export.cpp arch/Lights/LightsDriver_Export.h \
|
||||
arch/Lights/LightsDriver_SystemMessage.cpp arch/Lights/LightsDriver_SystemMessage.h
|
||||
arch/Lights/LightsDriver_SystemMessage.cpp arch/Lights/LightsDriver_SystemMessage.h \
|
||||
arch/Lights/LightsDriver_SextetStream.cpp arch/Lights/LightsDriver_SextetStream.h
|
||||
|
||||
LoadingWindow += arch/LoadingWindow/LoadingWindow_Null.h
|
||||
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
#include "global.h"
|
||||
#include "InputHandler_SextetStream.h"
|
||||
#include "PrefsManager.h"
|
||||
#include "RageLog.h"
|
||||
#include "RageThreads.h"
|
||||
#include "RageUtil.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
using namespace std;
|
||||
|
||||
// In so many words, ceil(n/6).
|
||||
#define NUMBER_OF_SEXTETS_FOR_BIT_COUNT(n) (((n) + 5) / 6)
|
||||
|
||||
#define FIRST_DEVICE DEVICE_JOY1
|
||||
|
||||
#define FIRST_JOY_BUTTON JOY_BUTTON_1
|
||||
#define LAST_JOY_BUTTON JOY_BUTTON_32
|
||||
#define COUNT_JOY_BUTTON ((LAST_JOY_BUTTON) - (FIRST_JOY_BUTTON) + 1)
|
||||
|
||||
#define FIRST_KEY KEY_OTHER_0
|
||||
#define LAST_KEY KEY_LAST_OTHER
|
||||
#define COUNT_KEY ((LAST_KEY) - (FIRST_KEY) + 1)
|
||||
|
||||
#define BUTTON_COUNT (COUNT_JOY_BUTTON + COUNT_KEY)
|
||||
|
||||
#define DEFAULT_TIMEOUT_MS 1000
|
||||
#define STATE_BUFFER_SIZE NUMBER_OF_SEXTETS_FOR_BIT_COUNT(BUTTON_COUNT)
|
||||
|
||||
namespace
|
||||
{
|
||||
class LineReader
|
||||
{
|
||||
protected:
|
||||
int timeout_ms;
|
||||
|
||||
public:
|
||||
LineReader()
|
||||
{
|
||||
timeout_ms = DEFAULT_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
virtual ~LineReader()
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool IsValid()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ideally, this method should return if timeout_ms passes
|
||||
// before a line becomes available. Actually doing this may
|
||||
// require some platform-specific non-blocking read capability.
|
||||
// This sort of thing could be implemented using e.g. POSIX
|
||||
// select() and Windows GetOverlappedResultEx(), both of which
|
||||
// have timeout parameters. (I get the sense that the
|
||||
// RageFileDriverTimeout class could be convinced to work, but
|
||||
// there isn't a lot of code using it, so I'm lacking the proper
|
||||
// examples.)
|
||||
//
|
||||
// If this method does block, almost everything will still work,
|
||||
// but the blocking may prevent the loop from checking
|
||||
// continueInputThread in a timely fashion. If the stream ceases
|
||||
// to produce new data before this object is destroyed, the
|
||||
// current thread will hang until the other side of the
|
||||
// connection closes the stream (or produces a line of data). A
|
||||
// workaround for that would be to have the far side of the
|
||||
// connection repeat its last line every second or so as a
|
||||
// keepalive.
|
||||
//
|
||||
// false (line undefined) if there is an error or EOF condition,
|
||||
// true (line = next line from stream) if a whole line is available,
|
||||
// true (line = "") if no error but still waiting for next line.
|
||||
virtual bool ReadLine(RString& line) = 0;
|
||||
};
|
||||
}
|
||||
|
||||
class InputHandler_SextetStream::Impl
|
||||
{
|
||||
private:
|
||||
InputHandler_SextetStream * handler;
|
||||
|
||||
protected:
|
||||
void ButtonPressed(const DeviceInput& di)
|
||||
{
|
||||
handler->ButtonPressed(di);
|
||||
}
|
||||
|
||||
uint8_t stateBuffer[STATE_BUFFER_SIZE];
|
||||
size_t timeout_ms;
|
||||
RageThread inputThread;
|
||||
bool continueInputThread;
|
||||
|
||||
// Construct and return the LineReader that makes sense for this
|
||||
// object. getLineReader() calls this; if the returned object claims
|
||||
// it is valid, it is returned. Otherwise, it is destroyed and NULL
|
||||
// is returned.
|
||||
virtual LineReader * getUnvalidatedLineReader() = 0;
|
||||
|
||||
inline void clearStateBuffer()
|
||||
{
|
||||
memset(stateBuffer, 0, STATE_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
inline void createThread()
|
||||
{
|
||||
continueInputThread = true;
|
||||
inputThread.SetName("SextetStream input thread");
|
||||
inputThread.Create(StartInputThread, this);
|
||||
}
|
||||
|
||||
LineReader * getLineReader()
|
||||
{
|
||||
LineReader * linereader = getUnvalidatedLineReader();
|
||||
if(linereader != NULL) {
|
||||
if(!linereader->IsValid()) {
|
||||
delete linereader;
|
||||
linereader = NULL;
|
||||
}
|
||||
}
|
||||
return linereader;
|
||||
}
|
||||
|
||||
public:
|
||||
Impl(InputHandler_SextetStream * _this)
|
||||
{
|
||||
LOG->Info("Number of button states supported by current InputHandler_SextetStream: %u",
|
||||
(unsigned)BUTTON_COUNT);
|
||||
continueInputThread = false;
|
||||
timeout_ms = DEFAULT_TIMEOUT_MS;
|
||||
|
||||
handler = _this;
|
||||
clearStateBuffer();
|
||||
createThread();
|
||||
}
|
||||
|
||||
virtual ~Impl()
|
||||
{
|
||||
if(inputThread.IsCreated()) {
|
||||
continueInputThread = false;
|
||||
inputThread.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
virtual void GetDevicesAndDescriptions(vector<InputDeviceInfo>& vDevicesOut)
|
||||
{
|
||||
vDevicesOut.push_back(InputDeviceInfo(FIRST_DEVICE, "SextetStream"));
|
||||
}
|
||||
|
||||
static int StartInputThread(void * p)
|
||||
{
|
||||
((Impl*) p)->RunInputThread();
|
||||
return 0;
|
||||
}
|
||||
|
||||
inline void GetNewState(uint8_t * buffer, RString& line)
|
||||
{
|
||||
size_t lineLen = line.length();
|
||||
size_t i, cursor;
|
||||
cursor = 0;
|
||||
memset(buffer, 0, STATE_BUFFER_SIZE);
|
||||
|
||||
// Copy from line to buffer until either it is full or we've run out
|
||||
// of characters. Characters outside the sextet code range
|
||||
// (0x30..0x6F) are skipped; the remaining characters have their two
|
||||
// high bits cleared.
|
||||
for(i = 0; i < lineLen; ++i) {
|
||||
char b = line[i];
|
||||
if((b >= 0x30) && (b <= 0x6F)) {
|
||||
buffer[cursor] = b & 0x3F;
|
||||
++cursor;
|
||||
if(cursor >= STATE_BUFFER_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline DeviceButton ButtonAtIndex(size_t index)
|
||||
{
|
||||
if(index < COUNT_JOY_BUTTON) {
|
||||
return enum_add2(FIRST_JOY_BUTTON, index);
|
||||
}
|
||||
else if(index < COUNT_JOY_BUTTON + COUNT_KEY) {
|
||||
return enum_add2(FIRST_KEY, index - COUNT_JOY_BUTTON);
|
||||
}
|
||||
else {
|
||||
return DeviceButton_Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
inline void ReactToChanges(const uint8_t * newStateBuffer)
|
||||
{
|
||||
InputDevice id = InputDevice(FIRST_DEVICE);
|
||||
uint8_t changes[STATE_BUFFER_SIZE];
|
||||
RageTimer now;
|
||||
|
||||
// XOR to find differences
|
||||
for(size_t i = 0; i < STATE_BUFFER_SIZE; ++i) {
|
||||
changes[i] = stateBuffer[i] ^ newStateBuffer[i];
|
||||
}
|
||||
|
||||
// Report on changes
|
||||
for(size_t m = 0; m < STATE_BUFFER_SIZE; ++m) {
|
||||
for(size_t n = 0; n < 6; ++n) {
|
||||
size_t bi = (m * 6) + n;
|
||||
if(bi < BUTTON_COUNT) {
|
||||
if(changes[m] & (1 << n)) {
|
||||
bool value = newStateBuffer[m] & (1 << n);
|
||||
LOG->Trace("SS button index %zu %s", bi, value ? "pressed" : "released");
|
||||
DeviceInput di = DeviceInput(id, ButtonAtIndex(bi), value, now);
|
||||
ButtonPressed(di);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update current state
|
||||
memcpy(stateBuffer, newStateBuffer, STATE_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
void RunInputThread()
|
||||
{
|
||||
RString line;
|
||||
LineReader * linereader;
|
||||
|
||||
LOG->Trace("Input thread started; getting line reader");
|
||||
linereader = getLineReader();
|
||||
|
||||
if(linereader == NULL) {
|
||||
LOG->Warn("Could not open line reader for SextetStream input");
|
||||
}
|
||||
else {
|
||||
LOG->Trace("Got line reader");
|
||||
while(continueInputThread) {
|
||||
LOG->Trace("Reading line");
|
||||
if(linereader->ReadLine(line)) {
|
||||
LOG->Trace("Got line: '%s'", line.c_str());
|
||||
if(line.length() > 0) {
|
||||
uint8_t newStateBuffer[STATE_BUFFER_SIZE];
|
||||
GetNewState(newStateBuffer, line);
|
||||
ReactToChanges(newStateBuffer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Error or EOF condition.
|
||||
LOG->Trace("Reached end of SextetStream input");
|
||||
continueInputThread = false;
|
||||
}
|
||||
}
|
||||
LOG->Info("SextetStream input stopped");
|
||||
delete linereader;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void InputHandler_SextetStream::GetDevicesAndDescriptions(vector<InputDeviceInfo>& vDevicesOut)
|
||||
{
|
||||
if(_impl != NULL) {
|
||||
_impl->GetDevicesAndDescriptions(vDevicesOut);
|
||||
}
|
||||
}
|
||||
|
||||
InputHandler_SextetStream::InputHandler_SextetStream()
|
||||
{
|
||||
_impl = NULL;
|
||||
}
|
||||
|
||||
InputHandler_SextetStream::~InputHandler_SextetStream()
|
||||
{
|
||||
if(_impl != NULL) {
|
||||
delete _impl;
|
||||
}
|
||||
}
|
||||
|
||||
// SextetStreamFromFile
|
||||
|
||||
REGISTER_INPUT_HANDLER_CLASS (SextetStreamFromFile);
|
||||
|
||||
#if defined(_WINDOWS)
|
||||
#define DEFAULT_INPUT_FILENAME "\\\\.\\pipe\\StepMania-Input-SextetStream"
|
||||
#else
|
||||
#define DEFAULT_INPUT_FILENAME "Data/StepMania-Input-SextetStream.in"
|
||||
#endif
|
||||
static Preference<RString> g_sSextetStreamInputFilename("SextetStreamInputFilename", DEFAULT_INPUT_FILENAME);
|
||||
|
||||
namespace
|
||||
{
|
||||
class StdCFileLineReader: public LineReader
|
||||
{
|
||||
private:
|
||||
// The buffer size isn't critical; the RString will simply be
|
||||
// extended until the line is done.
|
||||
static const size_t BUFFER_SIZE = 64;
|
||||
char buffer[BUFFER_SIZE];
|
||||
protected:
|
||||
std::FILE * file;
|
||||
|
||||
public:
|
||||
StdCFileLineReader(std::FILE * file)
|
||||
{
|
||||
LOG->Info("Starting InputHandler_SextetStreamFromFile from open std::FILE");
|
||||
this->file = file;
|
||||
}
|
||||
|
||||
StdCFileLineReader(const RString& filename)
|
||||
{
|
||||
LOG->Info("Starting InputHandler_SextetStreamFromFile from std::FILE with filename '%s'",
|
||||
filename.c_str());
|
||||
file = std::fopen(filename.c_str(), "rb");
|
||||
|
||||
if(file == NULL) {
|
||||
LOG->Warn("Error opening file '%s' for input (cstdio): %s", filename.c_str(),
|
||||
std::strerror(errno));
|
||||
}
|
||||
else {
|
||||
LOG->Info("File opened");
|
||||
// Disable buffering on the file
|
||||
std::setbuf(file, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
~StdCFileLineReader()
|
||||
{
|
||||
if(file != NULL) {
|
||||
std::fclose(file);
|
||||
}
|
||||
}
|
||||
|
||||
virtual bool IsValid()
|
||||
{
|
||||
return file != NULL;
|
||||
}
|
||||
|
||||
virtual bool ReadLine(RString& line)
|
||||
{
|
||||
bool afterFirst = false;
|
||||
size_t len;
|
||||
|
||||
line = "";
|
||||
|
||||
if(file != NULL) {
|
||||
while(fgets(buffer, BUFFER_SIZE, file) != NULL) {
|
||||
afterFirst = true;
|
||||
line += buffer;
|
||||
len = line.length();
|
||||
if(len > 0 && line[len - 1] == 0xA) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return afterFirst;
|
||||
}
|
||||
};
|
||||
|
||||
class StdCFileHandleImpl: public InputHandler_SextetStream::Impl
|
||||
{
|
||||
protected:
|
||||
std::FILE * file;
|
||||
|
||||
public:
|
||||
StdCFileHandleImpl(InputHandler_SextetStreamFromFile * handler, std::FILE * file) :
|
||||
InputHandler_SextetStream::Impl(handler)
|
||||
{
|
||||
this->file = file;
|
||||
}
|
||||
|
||||
virtual LineReader * getUnvalidatedLineReader()
|
||||
{
|
||||
return new StdCFileLineReader(this->file);
|
||||
}
|
||||
|
||||
virtual ~StdCFileHandleImpl()
|
||||
{
|
||||
// line reader dtor will close file for us
|
||||
}
|
||||
};
|
||||
|
||||
class StdCFileNameImpl: public InputHandler_SextetStream::Impl
|
||||
{
|
||||
protected:
|
||||
RString filename;
|
||||
|
||||
public:
|
||||
StdCFileNameImpl(InputHandler_SextetStreamFromFile * handler, const RString& filename) :
|
||||
InputHandler_SextetStream::Impl(handler)
|
||||
{
|
||||
this->filename = filename;
|
||||
}
|
||||
|
||||
virtual LineReader * getUnvalidatedLineReader()
|
||||
{
|
||||
return new StdCFileLineReader(filename);
|
||||
}
|
||||
|
||||
virtual ~StdCFileNameImpl()
|
||||
{
|
||||
// Nothing to destroy
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
InputHandler_SextetStreamFromFile::InputHandler_SextetStreamFromFile(FILE * file)
|
||||
{
|
||||
_impl = new StdCFileHandleImpl(this, file);
|
||||
}
|
||||
|
||||
InputHandler_SextetStreamFromFile::InputHandler_SextetStreamFromFile(const RString& filename)
|
||||
{
|
||||
_impl = new StdCFileNameImpl(this, filename);
|
||||
}
|
||||
|
||||
InputHandler_SextetStreamFromFile::InputHandler_SextetStreamFromFile()
|
||||
{
|
||||
_impl = new StdCFileNameImpl(this, g_sSextetStreamInputFilename);
|
||||
}
|
||||
|
||||
/*
|
||||
* Copyright © 2014 Peter S. May
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
* persons to whom the Software is furnished to do so, subject to the
|
||||
* following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
* USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#ifndef INPUT_HANDLER_SEXTETSTREAM
|
||||
#define INPUT_HANDLER_SEXTETSTREAM
|
||||
|
||||
#include "InputHandler.h"
|
||||
#include <cstdio>
|
||||
|
||||
class InputHandler_SextetStream: public InputHandler
|
||||
{
|
||||
public:
|
||||
InputHandler_SextetStream();
|
||||
virtual ~InputHandler_SextetStream();
|
||||
//virtual void Update();
|
||||
virtual void GetDevicesAndDescriptions(vector<InputDeviceInfo>& vDevicesOut);
|
||||
|
||||
public:
|
||||
class Impl;
|
||||
protected:
|
||||
Impl * _impl;
|
||||
};
|
||||
|
||||
// Note: InputHandler_SextetStreamFromFile uses blocking I/O. For the
|
||||
// handler thread to close in a timely fashion, the producer of data for the
|
||||
// file (e.g. the program at the other end of the pipe) must either close
|
||||
// the file or output and flush a line of data no less often than about once
|
||||
// per second, even if there has been no change. (Repeating the most recent
|
||||
// state accomplishes this without triggering any new events.) Either of
|
||||
// these interrupts the blocking read so that the loop can check its
|
||||
// continue flag.
|
||||
class InputHandler_SextetStreamFromFile: public InputHandler_SextetStream
|
||||
{
|
||||
public:
|
||||
// Note: In the current implementation, the filename (either the
|
||||
// `filename` parameter or the `SextetStreamInputFilename` setting) is
|
||||
// passed to fopen(), not a RageFile ctor, so specify the file to be
|
||||
// opened on the actual filesystem instead of the mapped filesystem. (I
|
||||
// couldn't get RageFile to work here, possibly because I haven't
|
||||
// determined how to disable buffering on an input file.)
|
||||
InputHandler_SextetStreamFromFile();
|
||||
InputHandler_SextetStreamFromFile(const RString& filename);
|
||||
|
||||
// The file object passed here must already be open and buffering should
|
||||
// be disabled. The file object will be closed in the destructor.
|
||||
InputHandler_SextetStreamFromFile(std::FILE * file);
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Copyright © 2014 Peter S. May
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
* persons to whom the Software is furnished to do so, subject to the
|
||||
* following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
* USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
@@ -0,0 +1,383 @@
|
||||
`InputHandler_SextetStream_*`
|
||||
=============================
|
||||
|
||||
Explanation
|
||||
-----------
|
||||
|
||||
This is a set of drivers (currently, just one driver) that accept button
|
||||
inputs encoded in a stream of character. By accepting this data from a
|
||||
stream, input can be produced by a separate program. Such a program can
|
||||
be implemented using any language/platform that supports reading from
|
||||
the desired input device (and writing to an output stream). If C++ isn't
|
||||
your thing, or if learning the guts of StepMania seems a little much
|
||||
just to implement an input driver, you're in the right place.
|
||||
|
||||
Quick start
|
||||
-----------
|
||||
|
||||
You'll need a working StepMania build with the driver
|
||||
`InputHandler_SextetStreamFromFile` enabled.
|
||||
|
||||
For this test, you'll run a test input program to verify that the driver
|
||||
is set up properly. This is a simple example of an *input program*, a
|
||||
program that will receive input from some arbitrary input source, encode
|
||||
the button states, and produce output to be read by StepMania. Because
|
||||
this script is for testing and diagnostics, input is accepted as
|
||||
keypresses on a GUI window. Actual input programs produce output in the
|
||||
same way, but will read input from different sources, such as data from
|
||||
a hardware interface (serial/parallel/USB).
|
||||
|
||||
### Set up `SextetStreamStdoutTest.jar`
|
||||
|
||||
You'll need the test input program,
|
||||
[`SextetStreamStdoutTest.jar`](https://github.com/psmay/SextetStreamStdoutTest/releases),
|
||||
as well as a working Java VM to run it. To ensure that the current
|
||||
environment is correct, simply run the program:
|
||||
|
||||
java -jar SextetStreamStdoutTest.jar
|
||||
|
||||
A GUI window should open. Click on this window and then try pressing
|
||||
some keys. When one of these keys is pressed, its code (number) appears;
|
||||
when released, the code disappears.
|
||||
|
||||
The output of this program on your console is the encoded sextets
|
||||
representing the state of the pressed keys. A new output line is
|
||||
produced immediately when a button is either pressed or released. An
|
||||
output line is also produced periodically (about once per second) if
|
||||
there have been no recent changes so that the driver is never left
|
||||
blocking for too long an interval.
|
||||
|
||||
Each output line is only as long as necessary to express the current
|
||||
state. While nothing is pressed, the output should be this line, about
|
||||
once per second:
|
||||
|
||||
@
|
||||
|
||||
Press and hold the up arrow. The output should be this line, again about
|
||||
once per second:
|
||||
|
||||
@@@@@@D
|
||||
|
||||
If instead you get a rapidly scrolling alternation like
|
||||
|
||||
@@@@@@D
|
||||
@
|
||||
@@@@@@D
|
||||
@
|
||||
@@@@@@D
|
||||
@
|
||||
@@@@@@D
|
||||
@
|
||||
|
||||
you should use your OS or window system settings to disable key repeat,
|
||||
at least for the duration of the test, to avoid an undesired rapid-fire
|
||||
effect.
|
||||
|
||||
### Linux
|
||||
|
||||
We assume that `SextetStreamStdoutTest.jar` is working (as outlined
|
||||
above) and that `$SM` is the root StepMania directory.
|
||||
|
||||
In `Preferences.ini`, set:
|
||||
|
||||
InputDrivers=X11,SextetStreamFromFile
|
||||
SextetStreamInputFilename=Data/StepMania-Input-SextetStream.in
|
||||
|
||||
(This also keeps X11-based keyboard input enabled. The X11 part can be
|
||||
removed later after setting up input mappings, if desired.)
|
||||
|
||||
Create the FIFO:
|
||||
|
||||
mkfifo "$SM/Data/StepMania-Input-SextetStream.in"
|
||||
|
||||
Run the test input program using the FIFO as output:
|
||||
|
||||
java -jar SextetStreamStdoutTest.jar > "$SM/Data/StepMania-Input-SextetStream.in"
|
||||
|
||||
While the input program is running, start StepMania. (While using the
|
||||
test program, use windowed mode to keep both StepMania and the input
|
||||
program visible.) Using the keyboard directly on the StepMania window,
|
||||
go to Options, Test Input. Switch to the test input program and try
|
||||
pressing some keys. If StepMania displays corresponding messages, the
|
||||
driver is working properly.
|
||||
|
||||
#### Serial port under Linux
|
||||
|
||||
FIXME: *This has not been tested at all.* The following is a guess at a
|
||||
synopsis of how it should work if you're lucky enough for everything to
|
||||
have fallen into place just so. (Please amend this message if you manage
|
||||
to get this running consistently.)
|
||||
|
||||
If you have a microcontroller running firmware that produces output
|
||||
compatible with the SextetStream protocol, you can use `socat` as the
|
||||
input program to pipe the data directly from the device via a serial
|
||||
port:
|
||||
|
||||
socat /dev/ttyUSB0,raw,echo=0,b115200 "$SM/Data/StepMania-Input-SextetStream.in"
|
||||
|
||||
Substitute your actual serial port device for `/dev/ttyUSB0` and the
|
||||
actual port speed for `115200`. (A low speed will not freeze StepMania,
|
||||
but may introduce an unacceptable input latency.)
|
||||
|
||||
TODO: Supply example Arduino/PIC/... firmware.
|
||||
|
||||
TODO: Supply example of full-duplex in cooperation with the SextetStream
|
||||
lights driver.
|
||||
|
||||
### Mac OS X
|
||||
|
||||
FIXME: *This has not been tested at all.* The following is a guess at a
|
||||
synopsis of how it should work if you're lucky enough for everything to
|
||||
have fallen into place just so. (Please amend this message if you manage
|
||||
to get this running consistently on Mac OS X.)
|
||||
|
||||
Follow the instructions for Linux, but change the `InputDrivers` setting
|
||||
to:
|
||||
|
||||
InputDrivers=HID,SextetStreamFromFile
|
||||
|
||||
(This also keeps the default HID-based input enabled. The HID part can
|
||||
be removed later after setting up input mappings, if desired.)
|
||||
|
||||
TODO: Serial examples.
|
||||
|
||||
### Windows
|
||||
|
||||
FIXME: *This has not been tested at all.* The following is a guess at a
|
||||
synopsis of how it should work if you're lucky enough for everything to
|
||||
have fallen into place just so. (Please amend this message if you manage
|
||||
to get this running consistently on Windows.)
|
||||
|
||||
We assume that `SextetStreamStdoutTest.jar` is working (as outlined
|
||||
above). You will also need
|
||||
[`createAndWritePipe`](https://github.com/psmay/windows-named-pipe-utils/releases)
|
||||
to be able to work with a named pipe from the console.
|
||||
|
||||
In `Preferences.ini`, set:
|
||||
|
||||
InputDrivers=DirectInput,SextetStreamFromFile
|
||||
SextetStreamInputFilename=\\.\pipe\StepMania-Input-SextetStream
|
||||
|
||||
(This also keeps the default DirectInput-based input enabled. The
|
||||
DirectInput part can be removed later after setting up input mappings,
|
||||
if desired.)
|
||||
|
||||
Run the input program and use `createAndWritePipe` to redirect the
|
||||
output into the named pipe:
|
||||
|
||||
java -jar SextetStreamStdoutTest.jar | createAndWritePipe StepMania-Input-SextetStream
|
||||
|
||||
While the input program is running, start StepMania. (While using the
|
||||
test program, use windowed mode to keep both StepMania and the input
|
||||
program visible.) Using the keyboard directly on the StepMania window,
|
||||
go to Options, Test Input. Switch to the test input program and try
|
||||
pressing some keys. If StepMania displays corresponding messages, the
|
||||
driver is working properly.
|
||||
|
||||
TODO: Serial examples.
|
||||
|
||||
Keyboard-based demo limitations
|
||||
-------------------------------
|
||||
|
||||
Due to the design of some keyboards and/or their drivers, the number of
|
||||
keys that can be held at one time, which keys can be pressed
|
||||
simultaneously, and which keys have priority over others may vary. The
|
||||
SextetStream driver itself has no such limitation and can process all
|
||||
buttons independently of each other if the input program is capable of
|
||||
providing such information.
|
||||
|
||||
Encoding
|
||||
--------
|
||||
|
||||
### Packing sextets
|
||||
|
||||
Values are encoded six bits at a time (hence "sextet"). The characters
|
||||
are made printable, non-whitespace ASCII so that attempting to read the
|
||||
data or pass it through a text-oriented channel will not cause problems.
|
||||
The encoding is similar in concept to base64 or uuencode, but in this
|
||||
scheme the low 6 bits remain unchanged.
|
||||
|
||||
Data is packed into the low 6 bits of a byte, then the two high bits are
|
||||
set in such a way that the result is printable, non-whitespace ASCII:
|
||||
|
||||
0x00-0x0F -> 0x40-0x4F
|
||||
0x10-0x1F -> 0x50-0x5F
|
||||
0x20-0x2F -> 0x60-0x6F
|
||||
0x30-0x3F -> 0x30-0x3F
|
||||
|
||||
(0x20-0x2F and 0x70-0x7F are avoided since they contain control or
|
||||
whitespace characters.)
|
||||
|
||||
To encode a 6-bit value `s` in this way, this transform may be used:
|
||||
|
||||
((s + 0x10) & 0x3F) + 0x30
|
||||
|
||||
### Bit meanings
|
||||
|
||||
This driver produces events for a virtual game controller with no axes
|
||||
and an arbitrary number of buttons. As with an actual gamepad, the
|
||||
in-game meaning of each button can be configured in StepMania.
|
||||
|
||||
Note that the maximum number of gamepad buttons supported by StepMania
|
||||
is currently 32. Higher codes are coded as keypresses for unknown keys
|
||||
(which is unusual for a game controller, but works here).
|
||||
|
||||
* Byte 0
|
||||
* 0x01 button B1
|
||||
* 0x02 button B2
|
||||
* 0x04 button B3
|
||||
* 0x08 button B4
|
||||
* 0x10 button B5
|
||||
* 0x20 button B6
|
||||
|
||||
* …
|
||||
|
||||
* Byte `n` for `n < 5`
|
||||
* 0x01 button B`6n+1`
|
||||
* 0x02 button B`6n+2`
|
||||
* 0x04 button B`6n+3`
|
||||
* 0x08 button B`6n+4`
|
||||
* 0x10 button B`6n+5`
|
||||
* 0x20 button B`6n+6`
|
||||
|
||||
* …
|
||||
|
||||
* Byte 5
|
||||
* 0x01 button B31
|
||||
* 0x02 button B32
|
||||
* 0x04 key unk 0
|
||||
* 0x08 key unk 1
|
||||
* 0x10 key unk 2
|
||||
* 0x20 key unk 3
|
||||
|
||||
* …
|
||||
|
||||
* Byte `n` for `n > 5`
|
||||
* 0x01 key unk `6(n-6)+4`
|
||||
* 0x02 key unk `6(n-6)+5`
|
||||
* 0x04 key unk `6(n-6)+6`
|
||||
* 0x08 key unk `6(n-6)+7`
|
||||
* 0x10 key unk `6(n-6)+8`
|
||||
* 0x20 key unk `6(n-6)+9`
|
||||
|
||||
A message is ended by terminating it with LF (0x0A) or CR LF (0x0D
|
||||
0x0A). Data bytes outside 0x30-0x6F are discarded. The message can be as
|
||||
large or as short as necessary, to encode all buttons. Any buttons not
|
||||
encoded in a message are understood to have the value 0.
|
||||
|
||||
These messages both indicate that buttons 4 and 6 are pressed, and all
|
||||
others are not:
|
||||
|
||||
# Note: 0x68 & 0x3F == 0x28; 0x40 & 0x3F == 0x00
|
||||
0x68 0x40 0x40 0x0A
|
||||
0x68 0x0A
|
||||
|
||||
The number of buttons supported by the implementation is currently the
|
||||
number of supported joy buttons plus the number of supported unknown
|
||||
keys (as of this writing: 32 + 321 = 353; you certainly shouldn't need
|
||||
this many). Overlong input lines are allowed and truncated. Because the
|
||||
number of bytes needed in an input line is proportional to the index of
|
||||
the highest-indexed bit currently in an on state, it's probably a good
|
||||
idea to keep that number low whenever possible, especially when using a
|
||||
low-speed input (such as a serial or network connection).
|
||||
|
||||
Filesystem
|
||||
----------
|
||||
|
||||
The current implementation of this driver uses C standard I/O (i.e.,
|
||||
`fopen()`, `fread()`, etc.) instead of the `RageFile` abstraction
|
||||
Therefore, any `RageFile`-based filesystem abstractions *are not*
|
||||
applied. Please keep this in mind when specifying the input path.
|
||||
|
||||
FIXME: This behavior is definitely subject to change should the
|
||||
following problem ever get worked out: I was unable to get `RageFile` to
|
||||
work in this context; I believe the cause is that I couldn't get a
|
||||
`RageFile` object to do unbuffered input. It may have been something
|
||||
else.
|
||||
|
||||
Pipes
|
||||
-----
|
||||
|
||||
The input of this driver is streamed from a file. At face value, this is
|
||||
not too useful. The actual intent is for the system operator to create a
|
||||
*named fifo* or *named pipe* that is being written to by some already
|
||||
running program, such as a program that reads button state data in over
|
||||
a serial connection. It just happens that opening a named fifo for
|
||||
reading can be accomplished in the same fashion as opening a file for
|
||||
reading, as long as buffering can be disabled. Since there's an easy,
|
||||
platform-ignorant way to do that, that's what we've done.
|
||||
|
||||
### Blocking behavior
|
||||
|
||||
This driver uses blocking I/O, but does so in a separate thread so that
|
||||
StepMania will read lines about as fast as they are written and there is
|
||||
no technical minimum data rate. However, the blocking read temporarily
|
||||
|
||||
particular, as StepMania exits, it may hang waiting for the input thread
|
||||
to finish until one of the following happens:
|
||||
|
||||
* The input program closes the stream
|
||||
* The input program outputs a line, allowing StepMania to end the
|
||||
input thread
|
||||
|
||||
So, for the purposes of this driver, it is good manners for an input
|
||||
program to periodically (e.g. once per second) repeat its current state
|
||||
so that the input thread is never left blocking for too long at a time.
|
||||
(Alternatively, if the input program has some way to determine that
|
||||
StepMania is exiting, it may just close the stream instead.)
|
||||
|
||||
### Linux (and some other unixish systems)
|
||||
|
||||
`mkfifo` is used to create a named FIFO. After this is done, two
|
||||
programs (one reading and one writing) open the FIFO as if it were some
|
||||
ordinary file and use ordinary file I/O to read or write.
|
||||
|
||||
For this driver, the input program and StepMania open the file for write
|
||||
and read, respectively.
|
||||
|
||||
Input programs producing output on stdout work trivially; the program's
|
||||
output is simply redirected to the FIFO.
|
||||
|
||||
### Windows
|
||||
|
||||
The client side of a Windows named pipe is fairly ordinary; the path to
|
||||
an already-open pipe can be opened and used as if it were an ordinary
|
||||
file. For this driver, StepMania is the client.
|
||||
|
||||
The server side of the pipe is a little trickier. Special Windows API
|
||||
calls are needed to create the pipe and then wait for the client to
|
||||
connect. Unlike in Linux, the FIFO goes away when it is closed. For this
|
||||
driver, the input program is the server.
|
||||
|
||||
Input programs producing output on stdout do not work trivially because
|
||||
named pipes are not eligible to be created or redirected to on the
|
||||
command line. Such a program can be redirected to a bridging program
|
||||
such as `createAndWritePipe`, which creates a pipe and then relays
|
||||
whatever is received on stdin to the pipe.
|
||||
|
||||
A Windows-specific input program might also just create and write a
|
||||
named pipe by itself.
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
Copyright © 2014 Peter S. May
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,289 @@
|
||||
#include "global.h"
|
||||
#include "LightsDriver_SextetStream.h"
|
||||
#include "PrefsManager.h"
|
||||
#include "RageLog.h"
|
||||
#include "RageUtil.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
using namespace std;
|
||||
|
||||
// Number of printable characters used to encode lights
|
||||
static const size_t CABINET_SEXTET_COUNT = 1;
|
||||
static const size_t CONTROLLER_SEXTET_COUNT = 6;
|
||||
|
||||
// Number of bytes to contain the full pack and a trailing LF
|
||||
static const size_t FULL_SEXTET_COUNT = CABINET_SEXTET_COUNT + (NUM_GameController * CONTROLLER_SEXTET_COUNT) + 1;
|
||||
|
||||
|
||||
// Serialization routines
|
||||
|
||||
// Encodes the low 6 bits of a byte as a printable, non-space ASCII
|
||||
// character (i.e., within the range 0x21-0x7E) such that the low 6 bits of
|
||||
// the character are the same as the input.
|
||||
inline uint8_t printableSextet(uint8_t data)
|
||||
{
|
||||
// Maps the 6-bit value into the range 0x30-0x6F, wrapped in such a way
|
||||
// that the low 6 bits of the result are the same as the data (so
|
||||
// decoding is trivial).
|
||||
//
|
||||
// 00nnnn -> 0100nnnn (0x4n)
|
||||
// 01nnnn -> 0101nnnn (0x5n)
|
||||
// 10nnnn -> 0110nnnn (0x6n)
|
||||
// 11nnnn -> 0011nnnn (0x3n)
|
||||
|
||||
// Put another way, the top 4 bits H of the output are determined from
|
||||
// the top two bits T of the input like so:
|
||||
// H = ((T + 1) mod 4) + 3
|
||||
|
||||
return ((data + (uint8_t)0x10) & (uint8_t)0x3F) + (uint8_t)0x30;
|
||||
}
|
||||
|
||||
// Packs 6 booleans into a 6-bit value
|
||||
inline uint8_t packPlainSextet(bool b0, bool b1, bool b2, bool b3, bool b4, bool b5)
|
||||
{
|
||||
return (uint8_t)(
|
||||
(b0 ? 0x01 : 0) |
|
||||
(b1 ? 0x02 : 0) |
|
||||
(b2 ? 0x04 : 0) |
|
||||
(b3 ? 0x08 : 0) |
|
||||
(b4 ? 0x10 : 0) |
|
||||
(b5 ? 0x20 : 0));
|
||||
}
|
||||
|
||||
// Packs 6 booleans into a printable sextet
|
||||
inline uint8_t packPrintableSextet(bool b0, bool b1, bool b2, bool b3, bool b4, bool b5)
|
||||
{
|
||||
return printableSextet(packPlainSextet(b0, b1, b2, b3, b4, b5));
|
||||
}
|
||||
|
||||
// Packs the cabinet lights into a printable sextet and adds it to a buffer
|
||||
inline size_t packCabinetLights(const LightsState *ls, uint8_t* buffer)
|
||||
{
|
||||
buffer[0] = packPrintableSextet(
|
||||
ls->m_bCabinetLights[LIGHT_MARQUEE_UP_LEFT],
|
||||
ls->m_bCabinetLights[LIGHT_MARQUEE_UP_RIGHT],
|
||||
ls->m_bCabinetLights[LIGHT_MARQUEE_LR_LEFT],
|
||||
ls->m_bCabinetLights[LIGHT_MARQUEE_LR_RIGHT],
|
||||
ls->m_bCabinetLights[LIGHT_BASS_LEFT],
|
||||
ls->m_bCabinetLights[LIGHT_BASS_RIGHT]);
|
||||
return CABINET_SEXTET_COUNT;
|
||||
}
|
||||
|
||||
// Packs the button lights for a controller into 6 printable sextets and
|
||||
// adds them to a buffer
|
||||
inline size_t packControllerLights(const LightsState *ls, GameController gc, uint8_t* buffer)
|
||||
{
|
||||
// Menu buttons
|
||||
buffer[0] = packPrintableSextet(
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_MENULEFT],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_MENURIGHT],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_MENUUP],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_MENUDOWN],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_START],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_SELECT]);
|
||||
|
||||
// Other non-sensors
|
||||
buffer[1] = packPrintableSextet(
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_BACK],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_COIN],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_OPERATOR],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_EFFECT_UP],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_EFFECT_DOWN],
|
||||
false);
|
||||
|
||||
// Sensors
|
||||
buffer[2] = packPrintableSextet(
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_01],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_02],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_03],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_04],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_05],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_06]);
|
||||
buffer[3] = packPrintableSextet(
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_07],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_08],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_09],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_10],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_11],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_12]);
|
||||
buffer[4] = packPrintableSextet(
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_13],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_14],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_15],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_16],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_17],
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_18]);
|
||||
buffer[5] = packPrintableSextet(
|
||||
ls->m_bGameButtonLights[gc][GAME_BUTTON_CUSTOM_19],
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false);
|
||||
|
||||
return CONTROLLER_SEXTET_COUNT;
|
||||
}
|
||||
|
||||
inline size_t packLine(uint8_t * buffer, const LightsState* ls)
|
||||
{
|
||||
size_t index = 0;
|
||||
|
||||
index += packCabinetLights(ls, &(buffer[index]));
|
||||
|
||||
FOREACH_ENUM(GameController, gc)
|
||||
{
|
||||
index += packControllerLights(ls, gc, &(buffer[index]));
|
||||
}
|
||||
|
||||
// Terminate with LF
|
||||
buffer[index++] = 0xA;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Private members/methods are kept out of the header using an opaque pointer `_impl`.
|
||||
// Google "pimpl idiom" for an explanation of what's going on and why it is (or might be) useful.
|
||||
|
||||
|
||||
// Implementation class
|
||||
|
||||
namespace
|
||||
{
|
||||
class Impl
|
||||
{
|
||||
protected:
|
||||
uint8_t lastOutput[FULL_SEXTET_COUNT];
|
||||
RageFile * out;
|
||||
|
||||
public:
|
||||
Impl(RageFile * file) {
|
||||
out = file;
|
||||
|
||||
// Ensure a non-match the first time
|
||||
lastOutput[0] = 0;
|
||||
}
|
||||
|
||||
virtual ~Impl() {
|
||||
if(out != NULL)
|
||||
{
|
||||
out->Flush();
|
||||
out->Close();
|
||||
SAFE_DELETE(out);
|
||||
}
|
||||
}
|
||||
|
||||
void Set(const LightsState * ls)
|
||||
{
|
||||
uint8_t buffer[FULL_SEXTET_COUNT];
|
||||
|
||||
packLine(buffer, ls);
|
||||
|
||||
// Only write if the message has changed since the last write.
|
||||
if(memcmp(buffer, lastOutput, FULL_SEXTET_COUNT) != 0)
|
||||
{
|
||||
if(out != NULL)
|
||||
{
|
||||
out->Write(buffer, FULL_SEXTET_COUNT);
|
||||
out->Flush();
|
||||
}
|
||||
|
||||
// Remember last message
|
||||
memcpy(lastOutput, buffer, FULL_SEXTET_COUNT);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// LightsDriver_SextetStream interface
|
||||
// (Wrapper for Impl)
|
||||
|
||||
#define IMPL ((Impl*)_impl)
|
||||
|
||||
LightsDriver_SextetStream::LightsDriver_SextetStream()
|
||||
{
|
||||
_impl = NULL;
|
||||
}
|
||||
|
||||
LightsDriver_SextetStream::~LightsDriver_SextetStream()
|
||||
{
|
||||
if(IMPL != NULL)
|
||||
{
|
||||
delete IMPL;
|
||||
}
|
||||
}
|
||||
|
||||
void LightsDriver_SextetStream::Set(const LightsState *ls)
|
||||
{
|
||||
if(IMPL != NULL)
|
||||
{
|
||||
IMPL->Set(ls);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// LightsDriver_SextetStreamToFile implementation
|
||||
|
||||
REGISTER_SOUND_DRIVER_CLASS(SextetStreamToFile);
|
||||
|
||||
#if defined(_WINDOWS)
|
||||
#define DEFAULT_OUTPUT_FILENAME "\\\\.\\pipe\\StepMania-Lights-SextetStream"
|
||||
#else
|
||||
#define DEFAULT_OUTPUT_FILENAME "Data/StepMania-Lights-SextetStream.out"
|
||||
#endif
|
||||
static Preference<RString> g_sSextetStreamOutputFilename("SextetStreamOutputFilename", DEFAULT_OUTPUT_FILENAME);
|
||||
|
||||
inline RageFile * openOutputStream(const RString& filename)
|
||||
{
|
||||
RageFile * file = new RageFile;
|
||||
|
||||
if(!file->Open(filename, RageFile::WRITE|RageFile::STREAMED))
|
||||
{
|
||||
LOG->Warn("Error opening file '%s' for output: %s", filename.c_str(), file->GetError().c_str());
|
||||
SAFE_DELETE(file);
|
||||
file = NULL;
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
LightsDriver_SextetStreamToFile::LightsDriver_SextetStreamToFile(RageFile * file)
|
||||
{
|
||||
_impl = new Impl(file);
|
||||
}
|
||||
|
||||
LightsDriver_SextetStreamToFile::LightsDriver_SextetStreamToFile(const RString& filename)
|
||||
{
|
||||
_impl = new Impl(openOutputStream(filename));
|
||||
}
|
||||
|
||||
LightsDriver_SextetStreamToFile::LightsDriver_SextetStreamToFile()
|
||||
{
|
||||
_impl = new Impl(openOutputStream(g_sSextetStreamOutputFilename));
|
||||
}
|
||||
|
||||
/*
|
||||
* Copyright © 2014 Peter S. May
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
* persons to whom the Software is furnished to do so, subject to the
|
||||
* following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
* USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
@@ -0,0 +1,64 @@
|
||||
#ifndef LightsDriver_SextetStream_H
|
||||
#define LightsDriver_SextetStream_H
|
||||
|
||||
/*
|
||||
* `LightsDriver_SextetStream` (abstract): Streams the light data (in
|
||||
* ASCII-safe sextets) to some output stream.
|
||||
*
|
||||
* * `LightsDriver_SextetStreamToFile`: Streams the light data to an
|
||||
* output file.
|
||||
* * The specified file may be a named pipe (Windows)/named fifo
|
||||
* (Linux, others). This makes it possible to implement an
|
||||
* out-of-process light controller without touching the StepMania
|
||||
* source and without using C++. See the included notes for
|
||||
* details.
|
||||
*/
|
||||
|
||||
#include "LightsDriver.h"
|
||||
#include "RageFile.h"
|
||||
|
||||
class LightsDriver_SextetStream : public LightsDriver
|
||||
{
|
||||
public:
|
||||
LightsDriver_SextetStream();
|
||||
virtual ~LightsDriver_SextetStream();
|
||||
virtual void Set(const LightsState *ls);
|
||||
protected:
|
||||
void * _impl;
|
||||
};
|
||||
|
||||
class LightsDriver_SextetStreamToFile : public LightsDriver_SextetStream
|
||||
{
|
||||
public:
|
||||
LightsDriver_SextetStreamToFile();
|
||||
LightsDriver_SextetStreamToFile(const RString& filename);
|
||||
|
||||
// The file object passed here should already be open, and will be
|
||||
// flushed, closed, and deleted in the destructor.
|
||||
LightsDriver_SextetStreamToFile(RageFile * file);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Copyright © 2014 Peter S. May
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
* persons to whom the Software is furnished to do so, subject to the
|
||||
* following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
* USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
@@ -0,0 +1,420 @@
|
||||
`LightsDriver_SextetStream_*`
|
||||
=============================
|
||||
|
||||
Explanation
|
||||
-----------
|
||||
|
||||
This is a set of drivers (currently, just one driver) that encode light
|
||||
states as text and stream the result to the desired output. By making
|
||||
the light data available as a readable stream, light control can be
|
||||
performed by another program. Such a program can be implemented using
|
||||
any language/platform that supports driving the desired output device
|
||||
(and reading from an input stream). If C++ isn't your thing, or if
|
||||
learning the guts of StepMania seems a little much just to implement a
|
||||
lights driver, you're in the right place.
|
||||
|
||||
Quick start
|
||||
-----------
|
||||
|
||||
You'll need a working StepMania build with the driver
|
||||
`LightsDriver_SextetStreamToFile` enabled.
|
||||
|
||||
For this test, you'll run a script, `SextetStreamStdinTest`, to verify
|
||||
that the driver is set up properly. The script is a simple example of a
|
||||
*lighting program*, a program that will receive input from StepMania and
|
||||
produce output using the data. Because the script is for testing and
|
||||
diagnostics, the is text in a console window. Actual lighting programs
|
||||
receive input in the same way, but will do different things with the
|
||||
output, such as sending commands out over a hardware interface
|
||||
(serial/parallel/USB) to control physical lights.
|
||||
|
||||
### Linux
|
||||
|
||||
You'll need the perl script
|
||||
[`SextetStreamStdinTest.pl`](https://gist.github.com/psmay/7f45a867c1ae8f88ec36#file-sextetstreamstdintest-pl).
|
||||
Set the executable bit (`chmod ug+x SextetStreamStdinTest.pl`).
|
||||
|
||||
We assume that `$SM` is the root StepMania directory.
|
||||
|
||||
In `Preferences.ini`, set:
|
||||
|
||||
LightsDriver=SextetStreamToFile
|
||||
SextetStreamOutputFilename=Data/StepMania-Lights-SextetStream.out
|
||||
|
||||
Create the FIFO:
|
||||
|
||||
mkfifo "$SM/Data/StepMania-Lights-SextetStream.out"
|
||||
|
||||
Run the test stdin-based lighting program, using the FIFO as input:
|
||||
|
||||
./SextetStreamStdinTest.pl < "$SM/Data/StepMania-Lights-SextetStream.out"
|
||||
|
||||
While the lighting program is running, start StepMania. (When using the
|
||||
test lighting program, windowed mode may be needed to keep the program
|
||||
visible.) The lighting program should display the light information.
|
||||
|
||||
When StepMania closes, the FIFO connection ends, causing the lighting
|
||||
program to exit as well.
|
||||
|
||||
#### Serial port under Linux
|
||||
|
||||
If you have a microcontroller running firmware that understands the
|
||||
SextetStream protocol, you can use `socat` as the lighting program to
|
||||
pipe the data directly to the device via a serial port:
|
||||
|
||||
socat "$SM/Data/StepMania-Lights-SextetStream.out" /dev/ttyUSB0,raw,echo=0,b115200
|
||||
|
||||
Substitute your actual serial port device for `/dev/ttyUSB0` and the
|
||||
actual port speed for `115200`. (But note that running at too low a
|
||||
speed may result in the output blocking, causing StepMania to freeze or
|
||||
stutter).
|
||||
|
||||
TODO: Supply example Arduino/PIC/... firmware.
|
||||
|
||||
TODO: Supply example of full-duplex in cooperation with the SextetStream
|
||||
input driver.
|
||||
|
||||
### Mac OS X
|
||||
|
||||
FIXME: *This has not been tested at all.* The following is a guess at a
|
||||
synopsis of how it should work if you're lucky enough for everything to
|
||||
have fallen into place just so. (Please amend this message if you manage
|
||||
to get this running consistently on Mac OS X.)
|
||||
|
||||
The instructions for Linux should work equally well for Mac OS X.
|
||||
|
||||
TODO: Serial examples.
|
||||
|
||||
### Windows
|
||||
|
||||
FIXME: *This has not been tested at all.* The following is a guess at a
|
||||
synopsis of how it should work if you're lucky enough for everything to
|
||||
have fallen into place just so. (Please amend this message if you manage
|
||||
to get this running consistently on Windows.)
|
||||
|
||||
You'll need:
|
||||
|
||||
* The Windows Script Host script
|
||||
[`SextetStreamStdinTest.wsf`](https://gist.github.com/psmay/7f45a867c1ae8f88ec36#file-sextetstreamstdintest-wsf),
|
||||
the lighting program
|
||||
* [`createAndReadPipe`](https://github.com/psmay/windows-named-pipe-utils/releases),
|
||||
to work with a named pipe from the console.
|
||||
|
||||
In `Preferences.ini`, set:
|
||||
|
||||
LightsDriver=SextetStreamToFile
|
||||
SextetStreamOutputFilename=\\.\pipe\StepMania-Lights-SextetStream
|
||||
|
||||
Use `createAndReadPipe` to create the named pipe, feeding the output
|
||||
into the test stdin-based lighting program:
|
||||
|
||||
createAndReadPipe StepMania-Lights-SextetStream | cscript //nologo SextetStreamStdinTest.wsf
|
||||
|
||||
While the lighting program is running, start StepMania. (When using the
|
||||
test lighting program, windowed mode may be needed to keep the program
|
||||
visible.) The lighting program should display the light information.
|
||||
|
||||
When StepMania closes, the pipe connection ends, causing the lighting
|
||||
program to exit as well.
|
||||
|
||||
TODO: Serial examples.
|
||||
|
||||
Encoding
|
||||
--------
|
||||
|
||||
### Packing sextets
|
||||
|
||||
Values are encoded six bits at a time (hence "sextet").
|
||||
|
||||
Data is packed into the low 6 bits of a byte, then the two high bits are
|
||||
set in such a way that the result is printable, non-whitespace ASCII:
|
||||
|
||||
0x00-0x0F -> 0x40-0x4F
|
||||
0x10-0x1F -> 0x50-0x5F
|
||||
0x20-0x2F -> 0x60-0x6F
|
||||
0x30-0x3F -> 0x30-0x3F
|
||||
|
||||
(0x20-0x2F and 0x70-0x7F are avoided since they contain control or
|
||||
whitespace characters.)
|
||||
|
||||
The characters are made printable so that attempting to read the output
|
||||
or pass it through a text-oriented channel will not cause problems.
|
||||
|
||||
In any case, decoding these values is trivial and involves no
|
||||
lookups—just do `& 0x3F`. (And even that may be optional, depending on
|
||||
the application.)
|
||||
|
||||
### Bit meanings
|
||||
|
||||
The driver repeatedly outputs a serialization of the current light state
|
||||
followed by an LF (0xA, `\n`), at least once each time any light should
|
||||
change (and currently no more often than that).
|
||||
|
||||
Currently, this message is 13 data bytes followed by LF. The following
|
||||
information is contained:
|
||||
|
||||
* Byte 0
|
||||
* 0x01 Marquee upper-left
|
||||
* 0x02 Marquee upper-right
|
||||
* 0x04 Marquee lower-left
|
||||
* 0x08 Marquee lower-right
|
||||
* 0x10 Bass left
|
||||
* 0x20 Bass right
|
||||
|
||||
* Byte 1
|
||||
* 0x01 Player 1 menu left
|
||||
* 0x02 Player 1 menu right
|
||||
* 0x04 Player 1 menu up
|
||||
* 0x08 Player 1 menu down
|
||||
* 0x10 Player 1 start
|
||||
* 0x20 Player 1 select
|
||||
|
||||
* Byte 2
|
||||
* 0x01 Player 1 back
|
||||
* 0x02 Player 1 coin
|
||||
* 0x04 Player 1 operator
|
||||
* 0x08 Player 1 effect up
|
||||
* 0x10 Player 1 effect down
|
||||
* 0x20 (reserved)
|
||||
|
||||
* Byte 3
|
||||
* 0x01 Player 1 #1
|
||||
* 0x02 Player 1 #2
|
||||
* 0x04 Player 1 #3
|
||||
* 0x08 Player 1 #4
|
||||
* 0x10 Player 1 #5
|
||||
* 0x20 Player 1 #6
|
||||
|
||||
* Byte 4
|
||||
* 0x01 Player 1 #7
|
||||
* 0x02 Player 1 #8
|
||||
* 0x04 Player 1 #9
|
||||
* 0x08 Player 1 #10
|
||||
* 0x10 Player 1 #11
|
||||
* 0x20 Player 1 #12
|
||||
|
||||
* Byte 5
|
||||
* 0x01 Player 1 #13
|
||||
* 0x02 Player 1 #14
|
||||
* 0x04 Player 1 #15
|
||||
* 0x08 Player 1 #16
|
||||
* 0x10 Player 1 #17
|
||||
* 0x20 Player 1 #18
|
||||
|
||||
* Byte 6
|
||||
* 0x01 Player 1 #19
|
||||
* 0x02 (reserved)
|
||||
* 0x04 (reserved)
|
||||
* 0x08 (reserved)
|
||||
* 0x10 (reserved)
|
||||
* 0x20 (reserved)
|
||||
|
||||
* Byte 7
|
||||
* 0x01 Player 2 menu left
|
||||
* 0x02 Player 2 menu right
|
||||
* 0x04 Player 2 menu up
|
||||
* 0x08 Player 2 menu down
|
||||
* 0x10 Player 2 start
|
||||
* 0x20 Player 2 select
|
||||
|
||||
* Byte 8
|
||||
* 0x01 Player 2 back
|
||||
* 0x02 Player 2 coin
|
||||
* 0x04 Player 2 operator
|
||||
* 0x08 Player 2 effect up
|
||||
* 0x10 Player 2 effect down
|
||||
* 0x20 (reserved)
|
||||
|
||||
* Byte 9
|
||||
* 0x01 Player 2 #1
|
||||
* 0x02 Player 2 #2
|
||||
* 0x04 Player 2 #3
|
||||
* 0x08 Player 2 #4
|
||||
* 0x10 Player 2 #5
|
||||
* 0x20 Player 2 #6
|
||||
|
||||
* Byte 10
|
||||
* 0x01 Player 2 #7
|
||||
* 0x02 Player 2 #8
|
||||
* 0x04 Player 2 #9
|
||||
* 0x08 Player 2 #10
|
||||
* 0x10 Player 2 #11
|
||||
* 0x20 Player 2 #12
|
||||
|
||||
* Byte 11
|
||||
* 0x01 Player 2 #13
|
||||
* 0x02 Player 2 #14
|
||||
* 0x04 Player 2 #15
|
||||
* 0x08 Player 2 #16
|
||||
* 0x10 Player 2 #17
|
||||
* 0x20 Player 2 #18
|
||||
|
||||
* Byte 12
|
||||
* 0x01 Player 2 #19
|
||||
* 0x02 (reserved)
|
||||
* 0x04 (reserved)
|
||||
* 0x08 (reserved)
|
||||
* 0x10 (reserved)
|
||||
* 0x20 (reserved)
|
||||
|
||||
Above, `Player 1 #n` and `Player 2 #n` refer to StepMania's internal
|
||||
`GAME_BUTTON_CUSTOM_nn` values, whose meaning depends on the game.
|
||||
Currently, these mappings are:
|
||||
|
||||
* dance
|
||||
* 1 Pad left
|
||||
* 2 Pad right
|
||||
* 3 Pad up
|
||||
* 4 Pad down
|
||||
* 5 Pad up-left
|
||||
* 6 Pad up-right
|
||||
|
||||
* techno: Same as dance, plus
|
||||
* 7 Pad center
|
||||
* 8 Pad down-left
|
||||
* 9 Pad down-right
|
||||
|
||||
* pump
|
||||
* 1 Pad up-left
|
||||
* 2 Pad up-right
|
||||
* 3 Pad center
|
||||
* 4 Pad down-left
|
||||
* 5 Pad down-right
|
||||
|
||||
* kb7
|
||||
* 1-7 Keys 1-7
|
||||
|
||||
* ez2
|
||||
* 1 Foot upper-left
|
||||
* 2 Foot upper-right
|
||||
* 3 Foot down
|
||||
* 4 Hand upper-left
|
||||
* 5 Hand upper-right
|
||||
* 6 Hand lower-left
|
||||
* 7 Hand lower-right
|
||||
|
||||
* para
|
||||
* 1 Button left
|
||||
* 2 Button up-left
|
||||
* 3 Button up
|
||||
* 4 Button up-right
|
||||
* 5 Button right
|
||||
|
||||
* ds3ddx
|
||||
* 1 Hand left
|
||||
* 2 Foot down-left
|
||||
* 3 Foot up-left
|
||||
* 4 Hand up
|
||||
* 5 Hand down
|
||||
* 6 Foot up-right
|
||||
* 7 Foot down-right
|
||||
* 8 Hand right
|
||||
|
||||
* beat
|
||||
* 1-7 Keys 1-7
|
||||
* 8 Scratch up
|
||||
* 9 Scratch down
|
||||
|
||||
* maniax
|
||||
* 1 Sensor over-left
|
||||
* 2 Sensor over-right
|
||||
* 3 Sensor under-left
|
||||
* 4 Sensor under-right
|
||||
|
||||
* popn
|
||||
* 1 Button left white
|
||||
* 2 Button left yellow
|
||||
* 3 Button left green
|
||||
* 4 Button left blue
|
||||
* 5 Button center
|
||||
* 6 Button right blue
|
||||
* 7 Button right green
|
||||
* 8 Button right yellow
|
||||
* 9 Button right white
|
||||
|
||||
Filesystem
|
||||
----------
|
||||
|
||||
The current implementation of this driver uses a `RageFile` for output.
|
||||
Therefore, any `RageFile`-based filesystem abstractions *are* applied.
|
||||
Please keep this in mind when specifying the input path.
|
||||
|
||||
FIXME: This behavior is probably permanent, but if it is not possible to
|
||||
open a Windows pipe using `RageFile`, `RageFile` should probably change
|
||||
so that it is.
|
||||
|
||||
Pipes
|
||||
-----
|
||||
|
||||
The output of this driver streams to a file. At face value, this is not
|
||||
too useful. The actual intent is for the system operator to create a
|
||||
*named fifo* or *named pipe* that is being listened to by some already
|
||||
running program, such as a program that sends light data out over a
|
||||
serial connection. It just happens that opening a named fifo for writing
|
||||
can be accomplished in the same fashion as opening a file for writing.
|
||||
Since there's an easy, platform-ignorant way to do that, that's what
|
||||
we've done.
|
||||
|
||||
Note that this uses blocking I/O, so your light processing program must
|
||||
make sure to empty the fifo as quickly as it gets filled. If this
|
||||
driver's write blocks, parts of the application stall. If the program is
|
||||
non-trivial, using a separate thread dedicated to reading may be useful.
|
||||
|
||||
How you actually start up the other end of the connection depends on
|
||||
your platform.
|
||||
|
||||
### Linux (and some other unixish systems)
|
||||
|
||||
`mkfifo` is used to create a named FIFO. After this is done, two
|
||||
programs (one reading and one writing) open the FIFO as if it were some
|
||||
ordinary file and use ordinary file I/O to read or write.
|
||||
|
||||
For this driver, the lighting program and StepMania open the file for
|
||||
read and write, respectively.
|
||||
|
||||
Lighting programs accepting input on stdin work trivially; the FIFO is
|
||||
simply redirected into the program's stdin.
|
||||
|
||||
### Windows
|
||||
|
||||
The client side of a Windows named pipe is fairly ordinary; the path to
|
||||
an already-open pipe can be opened and used as if it were an ordinary
|
||||
file. For this driver, StepMania is the client.
|
||||
|
||||
The server side of the pipe is a little trickier. Special Windows API
|
||||
calls are needed to create the pipe and then wait for the client to
|
||||
connect. Unlike in Linux, the FIFO goes away when it is closed. For this
|
||||
driver, the lighting program is the server.
|
||||
|
||||
Lighting programs accepting input on stdin do not work trivially because
|
||||
named pipes are not eligible to be created or redirected on the command
|
||||
line. A bridging program such as `createAndReadPipe` can be used to
|
||||
create a pipe and relay all of its input to stdout, which can then be
|
||||
redirected.
|
||||
|
||||
A Windows-specific lighting program might also just create and read a
|
||||
named pipe by itself.
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
Copyright © 2014 Peter S. May
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Reference in New Issue
Block a user