Hard Light Productions Forums

Modding, Mission Design, and Coding => FS2 Open Coding - The Source Code Project (SCP) => Cross-Platform Development => Topic started by: SiriusGrey on December 31, 2009, 04:44:44 am

Title: [PATCH] Force Feedback Support for Linux
Post by: SiriusGrey on December 31, 2009, 04:44:44 am
Hi all,

Since I just managed to get the force-feedback for my Saitek Cyborg Evo Force working on linux I wanted to try it in my favorite game - but alas, on linux the force-feedback functions were stubbed!

It was quite easy however to port the DirectInput code to linux, it even shrank about 200 lines in the process...
One thing that is not implemented yet is joystick discovery, so my joysticks event file /dev/input/event10 (it always gets the same for me) is currently hardcoded. If anyone knows how to get STL to divulge its internal connection to the joystick I would be very happy.

Currently only devices that support at least 9 effects loaded into memory will work correctly, however I'll work on this some more if somebody is interested. Other devices will only play the effects that were played first.

Here is the patch that patches SVN Head (and 3.6.10) and works for me:

Code: [Select]
--- src_fs2_open_svn/fs2_open/code/io/joy-unix.cpp 2009-12-30 22:41:14.000000000 +0100
+++ src_fs2_open_3_6_10/code/io/joy-unix.cpp 2009-12-19 20:29:14.000000000 +0100
@@ -9,6 +9,8 @@
 
 #ifndef WIN32 // Goober5000
 
+#define LINUX_FORCE_FEEDBACK
+
 #include "globalincs/pstypes.h"
 #include "io/joy.h"
 #include "math/fix.h"
@@ -64,6 +66,7 @@
  sdljoy = NULL;
 
  SDL_QuitSubSystem (SDL_INIT_JOYSTICK);
+        joy_ff_shutdown();
 }
 
 void joy_get_caps (int max)
@@ -537,6 +540,7 @@
  SDL_EventState( SDL_JOYHATMOTION, SDL_ENABLE );
 
  Joy_inited = 1;
+        joy_ff_init();
 
  return joy_num_sticks;
 }
@@ -572,6 +576,14 @@
  return 1;
 }
 
+
+#ifndef LINUX_FORCE_FEEDBACK
+
+void joy_ff_shutdown()
+{
+// STUB_FUNCTION;
+}
+
 void joy_ff_adjust_handling(int speed)
 {
 // STUB_FUNCTION;
@@ -632,7 +644,7 @@
 // STUB_FUNCTION;
 }
 
-void joy_ff_play_vector_effect(vec3d *v, float scaler)
+void joy_ff_play_vector_effect(vec3d *v, float scaler);
 {
 // STUB_FUNCTION;
 }
@@ -642,4 +654,597 @@
  joy_ff_afterburn_off();
 }
 
+
+#else // LINUX_FORCE_FEEDBACK
+
+#include "math/vecmat.h"
+#include <stdio.h>
+#include <linux/input.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+
+int joy_ff_handling_scaler;
+int Joy_ff_enabled = 0;
+int Joy_ff_directional_hit_effect_enabled = 1;
+const int ff_percent = 250; // max 327
+
+int fdFFDevice;
+int nMaxEffects;
+
+int joy_ff_create_effects();
+void joy_ff_stop_effects();
+
+ff_effect pHitEffect1;
+ff_effect pHitEffect2;
+ff_effect pShootEffect;
+ff_effect pSecShootEffect;
+ff_effect pSpring;
+ff_effect pAfterburn1;
+ff_effect pAfterburn2;
+ff_effect pDock;
+ff_effect pDeathroll1;
+ff_effect pDeathroll2;
+ff_effect pExplode;
+ff_effect * ff_all_effects[] = {&pHitEffect1,&pHitEffect2,
+                &pShootEffect,&pSecShootEffect,&pSpring,
+                &pAfterburn1,&pAfterburn2,&pDock,
+                &pDeathroll1,&pDeathroll2,&pExplode,0};
+
+const char * device_file_name = "/dev/input/event10";
+
+void joy_ff_afterburn_off();
+
+#define BITS_PER_LONG (sizeof(long) * 8)
+#define OFF(x)  ((x)%BITS_PER_LONG)
+#define BIT(x)  (1UL<<OFF(x))
+#define LONG(x) ((x)/BITS_PER_LONG)
+#define test_bit(bit, array)    ((array[LONG(bit)] >> OFF(bit)) & 1)
+
+int joy_ff_init()
+{
+        int ff_enabled;
+ Joy_ff_enabled = 0; // Assume no force feedback
+        nMaxEffects = 0;
+        printf("Trying to open FF joystick\n");
+        // Open device */
+        fdFFDevice = open(device_file_name, O_RDWR);
+        if (fdFFDevice != -1) {
+            ff_enabled = 1;
+            printf("Device %s opened\n", device_file_name);
+        }
+        printf("Trying to open FF joystick - froody!\n");
+
+ if (ff_enabled) {
+                printf("Trying to open FF joystick - querying features!\n");
+                /* Query device */
+                unsigned long features[4];
+                if (ioctl(fdFFDevice, EVIOCGBIT(EV_FF, sizeof(unsigned long) * 4), features) == -1) {
+                    perror("Ioctl query");
+                    return -1;
+                }
+                if (!(test_bit(ABS_X, features) && test_bit(ABS_Y, features))) {
+                    perror("No Axis!");
+                    return -1;
+                }
+
+                if (test_bit(FF_CONSTANT, features)) {
+                    nprintf(("Constant "));};
+                if (test_bit(FF_PERIODIC, features)) {
+                    nprintf(("Periodic "));};
+                if (test_bit(FF_SPRING, features)) {
+                    nprintf(("Spring "));
+                };
+
+                ioctl(fdFFDevice, EVIOCGEFFECTS, &nMaxEffects);
+                for (int i = 0; i < nMaxEffects; i++) ioctl(fdFFDevice, EVIOCRMFF, i);
+ if (joy_ff_create_effects())
+                    return -1;
+ Joy_ff_enabled = 1;
+ }
+
+ return 0;
+}
+
+void joy_ff_shutdown()
+{
+ if (Joy_ff_enabled) {
+ joy_ff_stop_effects();
+                for (int i = 0; i < nMaxEffects; i++) ioctl(fdFFDevice, EVIOCRMFF, i);
+                close(fdFFDevice);
+ }
+}
+
+int joy_ff_handle_error(int ec, char *eff_name = NULL)
+{
+ if (ec != 0) {
+ if (eff_name)
+ nprintf(("Joystick", "FF: Error for %s: %i\n", eff_name, ec));
+ else
+ nprintf(("Joystick", "FF: Error: %i\n", ec));
+ }
+        //TODO: handle loss of connection
+ return ec;
+}
+
+
+void joy_ff_reload_effect(ff_effect *eff, const char *name)
+{
+        nprintf(("Joystick", "FF: Reloading effect %s\n", name));
+        if (eff->id >= 0) ioctl(fdFFDevice, EVIOCRMFF, eff->id);
+        eff->id = -1;
+        ioctl(fdFFDevice, EVIOCSFF, eff);
+        printf("Effect %s has ID %i\n",name,eff->id);
+/*        for (int i = 0; ff_all_effects[i] != 0; i++) {
+            if ((ff_all_effects[i]->id == eff->id) && (eff != ff_all_effects[i])) {
+                printf("WARNING: Deleting overlapping event %i!\n",i);
+                ioctl(fdFFDevice, EVIOCRMFF, i);
+                ff_all_effects[i]->id = -1;
+            }
+        }*/
+}
+
+void joy_ff_start_effect(ff_effect *eff, const char *name)
+{
+        nprintf(("Joystick", "FF: Starting effect %s\n", name));
+        if (eff->id == -1) joy_ff_reload_effect(eff,name);
+        if (eff->id == -1) {
+            perror("Not playing effect - Upload failed.");
+            return;
+        }
+        struct input_event play;
+        play.type = EV_FF;
+        play.code = eff->id;
+        play.value = 1;
+        write(fdFFDevice, (const void*) &play, sizeof(play));
+}
+
+void joy_ff_stop_effect(ff_effect *eff, const char *name)
+{
+        if (eff->id == -1) {
+            return;
+        }
+ nprintf(("Joystick", "FF: Stopping effect %s\n", name));
+        struct input_event play;
+        play.type = EV_FF;
+        play.code = eff->id;
+        play.value = 0;
+        write(fdFFDevice, (const void*) &play, sizeof(play));
+}
+
+int joy_ff_create_effects()
+{
+
+
+        pHitEffect1.type = FF_CONSTANT;
+        pHitEffect1.id = -1;
+        pHitEffect1.u.constant.level = 100*ff_percent;
+        pHitEffect1.direction = 0;
+        pHitEffect1.u.constant.envelope.attack_length = 0;
+        pHitEffect1.u.constant.envelope.attack_level = 100*ff_percent;
+        pHitEffect1.u.constant.envelope.fade_length = 120;
+        pHitEffect1.u.constant.envelope.fade_level = 1;
+        pHitEffect1.trigger.button = 0;
+        pHitEffect1.trigger.interval = 0;
+        pHitEffect1.replay.length = 300;
+        pHitEffect1.replay.delay = 0;
+
+        pHitEffect2.type = FF_PERIODIC;
+        pHitEffect2.id = -1;
+        pHitEffect2.u.periodic.waveform = FF_SINE;
+        pHitEffect2.u.periodic.period = 100;       /* 0.1 second */
+        pHitEffect2.u.periodic.magnitude = 100*ff_percent; 
+        pHitEffect2.u.periodic.offset = 0;
+        pHitEffect2.u.periodic.phase = 0;
+        pHitEffect2.direction = 0x4000;  /* Along X axis */
+        pHitEffect2.u.periodic.envelope.attack_length = 100;
+        pHitEffect2.u.periodic.envelope.attack_level = 0;
+        pHitEffect2.u.periodic.envelope.fade_length = 100;
+        pHitEffect2.u.periodic.envelope.fade_level = 0;
+        pHitEffect2.trigger.button = 0;
+        pHitEffect2.trigger.interval = 0;
+        pHitEffect2.replay.length = 300;  /* ms */
+        pHitEffect2.replay.delay = 0;
+
+        pShootEffect.type = FF_PERIODIC;
+        pShootEffect.id = -1;
+        pShootEffect.u.periodic.waveform = FF_SAW_DOWN;
+        pShootEffect.u.periodic.period = 20;       /* ms */
+        pShootEffect.u.periodic.magnitude = 100*ff_percent; 
+        pShootEffect.u.periodic.offset = 0;
+        pShootEffect.u.periodic.phase = 0;
+        pShootEffect.direction = 0;
+        pShootEffect.u.periodic.envelope.attack_length = 0;
+        pShootEffect.u.periodic.envelope.attack_level = 0;
+        pShootEffect.u.periodic.envelope.fade_length = 120;
+        pShootEffect.u.periodic.envelope.fade_level = 0;
+        pShootEffect.trigger.button = 0;
+        pShootEffect.trigger.interval = 0;
+        pShootEffect.replay.length = 160;  /* ms */
+        pShootEffect.replay.delay = 0;
+
+        pSecShootEffect.type = FF_CONSTANT;
+        pSecShootEffect.id = -1;
+        pSecShootEffect.u.constant.level = 100*ff_percent;
+        pSecShootEffect.direction = 0;
+        pSecShootEffect.u.constant.envelope.attack_length = 50;
+        pSecShootEffect.u.constant.envelope.attack_level = 100*ff_percent;
+        pSecShootEffect.u.constant.envelope.fade_length = 100;
+        pSecShootEffect.u.constant.envelope.fade_level = 1;
+        pSecShootEffect.trigger.button = 0;
+        pSecShootEffect.trigger.interval = 0;
+        pSecShootEffect.replay.length = 200;
+        pSecShootEffect.replay.delay = 0;
+
+        pSpring.type = FF_SPRING;
+        pSpring.id = -1;
+        pSpring.u.condition[0].right_saturation = 0x7fff;
+        pSpring.u.condition[0].left_saturation = 0x7fff;
+        pSpring.u.condition[0].right_coeff = 0x7fff;
+        pSpring.u.condition[0].left_coeff = 0x7fff;
+        pSpring.u.condition[0].deadband = 0x0;
+        pSpring.u.condition[0].center = 0x0;
+        pSpring.u.condition[1] = pSpring.u.condition[0];
+        pSpring.trigger.button = 0;
+        pSpring.trigger.interval = 0;
+        pSpring.replay.length = -1;  /* 20 seconds */
+        pSpring.replay.delay = 0;
+
+        pAfterburn1.type = FF_PERIODIC;
+        pAfterburn1.id = -1;
+        pAfterburn1.u.periodic.waveform = FF_SINE;
+        pAfterburn1.u.periodic.period = 20;       /* ms */
+        pAfterburn1.u.periodic.magnitude = 80*ff_percent;
+        pAfterburn1.u.periodic.offset = 0;
+        pAfterburn1.u.periodic.phase = 0;
+        pAfterburn1.direction = 0;
+        pAfterburn1.u.periodic.envelope.attack_length = 0;
+        pAfterburn1.u.periodic.envelope.attack_level = 0;
+        pAfterburn1.u.periodic.envelope.fade_length = 0;
+        pAfterburn1.u.periodic.envelope.fade_level = 0;
+        pAfterburn1.trigger.button = 0;
+        pAfterburn1.trigger.interval = 0;
+        pAfterburn1.replay.length = -1;  /* ms */
+        pAfterburn1.replay.delay = 0;
+
+        pAfterburn2.type = FF_PERIODIC;
+        pAfterburn2.id = -1;
+        pAfterburn2.u.periodic.waveform = FF_SINE;
+        pAfterburn2.u.periodic.period = 120;       /* ms */
+        pAfterburn2.u.periodic.magnitude = 44*ff_percent; 
+        pAfterburn2.u.periodic.offset = 0;
+        pAfterburn2.u.periodic.phase = 0;
+        pAfterburn2.direction = 0x4000;
+        pAfterburn2.u.periodic.envelope.attack_length = 0;
+        pAfterburn2.u.periodic.envelope.attack_level = 0;
+        pAfterburn2.u.periodic.envelope.fade_length = 0;
+        pAfterburn2.u.periodic.envelope.fade_level = 0;
+        pAfterburn2.trigger.button = 0;
+        pAfterburn2.trigger.interval = 0;
+        pAfterburn2.replay.length = -1;  /* ms */
+        pAfterburn2.replay.delay = 0;
+
+        pDock.type = FF_PERIODIC;
+        pDock.id = -1;
+        pDock.u.periodic.waveform = FF_SQUARE;
+        pDock.u.periodic.period = 100;       /* ms */
+        pDock.u.periodic.magnitude = 40*ff_percent; 
+        pDock.u.periodic.offset = 0;
+        pDock.u.periodic.phase = 0;
+        pDock.direction = 0x4000;
+        pDock.u.periodic.envelope.attack_length = 0;
+        pDock.u.periodic.envelope.attack_level = 0;
+        pDock.u.periodic.envelope.fade_length = 0;
+        pDock.u.periodic.envelope.fade_level = 0;
+        pDock.trigger.button = 0;
+        pDock.trigger.interval = 0;
+        pDock.replay.length = 125;  /* ms */
+        pDock.replay.delay = 0;
+
+        pExplode.type = FF_PERIODIC;
+        pExplode.id = -1;
+        pExplode.u.periodic.waveform = FF_SAW_DOWN;
+        //pExplode.u.periodic.period = 20;       /* ms */
+        pExplode.u.periodic.period = 50;       /* ms - 50 is better :) */
+        pExplode.u.periodic.magnitude = 100*ff_percent; 
+        pExplode.u.periodic.offset = 0;
+        pExplode.u.periodic.phase = 0;
+        pExplode.direction = 0x4000;
+        pExplode.u.periodic.envelope.attack_length = 0;
+        pExplode.u.periodic.envelope.attack_level = 0;
+        pExplode.u.periodic.envelope.fade_length = 500;
+        pExplode.u.periodic.envelope.fade_level = 0;
+        pExplode.trigger.button = 0;
+        pExplode.trigger.interval = 0;
+        pExplode.replay.length = 500;  /* ms */
+        pExplode.replay.delay = 0;
+
+        pDeathroll1.type = FF_PERIODIC;
+        pDeathroll1.id = -1;
+        pDeathroll1.u.periodic.waveform = FF_SINE;
+        pDeathroll1.u.periodic.period = 200;       /* ms */
+        pDeathroll1.u.periodic.magnitude = 100*ff_percent; 
+        pDeathroll1.u.periodic.offset = 0;
+        pDeathroll1.u.periodic.phase = 0x2000;
+        pDeathroll1.direction = 0;
+        pDeathroll1.u.periodic.envelope.attack_length = 2000;
+        pDeathroll1.u.periodic.envelope.attack_level = 0;
+        pDeathroll1.u.periodic.envelope.fade_length = 0;
+        pDeathroll1.u.periodic.envelope.fade_level = 0;
+        pDeathroll1.trigger.button = 0;
+        pDeathroll1.trigger.interval = 0;
+        pDeathroll1.replay.length = -1;  /* ms */
+        pDeathroll1.replay.delay = 0;
+
+        pDeathroll2.type = FF_PERIODIC;
+        pDeathroll2.id = -1;
+        pDeathroll2.u.periodic.waveform = FF_SINE;
+        pDeathroll2.u.periodic.period = 200;       /* ms */
+        pDeathroll2.u.periodic.magnitude = 100*ff_percent; 
+        pDeathroll2.u.periodic.offset = 0;
+        pDeathroll2.u.periodic.phase = 0;
+        pDeathroll2.direction = 0x4000;
+        pDeathroll2.u.periodic.envelope.attack_length = 2000;
+        pDeathroll2.u.periodic.envelope.attack_level = 0;
+        pDeathroll2.u.periodic.envelope.fade_length = 0;
+        pDeathroll2.u.periodic.envelope.fade_level = 0;
+        pDeathroll2.trigger.button = 0;
+        pDeathroll2.trigger.interval = 0;
+        pDeathroll2.replay.length = -1;  /* ms */
+        pDeathroll2.replay.delay = 0;
+
+ return 0;
+}
+
+void joy_ff_stop_effects()
+{
+ joy_ff_afterburn_off();
+}
+
+void joy_ff_mission_init(vec3d v)
+{
+ v.xyz.z = 0.0f;
+// joy_ff_handling_scaler = (int) ((vm_vec_mag(&v) - 1.3f) * 10.5f);
+ joy_ff_handling_scaler = (int) ((vm_vec_mag(&v) + 1.3f) * 5.0f);
+// joy_ff_handling_scaler = (int) (vm_vec_mag(&v) * 7.5f);
+}
+
+int handler_adjust = -1;
+void joy_ff_adjust_handling(int speed)
+{
+ int v;
+
+ v = speed * joy_ff_handling_scaler * 2 / 3;
+// v += joy_ff_handling_scaler * joy_ff_handling_scaler * 6 / 7 + 250;
+ v += joy_ff_handling_scaler * 45 - 500;
+ if (v > 10000)
+ v = 10000;
+
+        if (handler_adjust != v) {
+                pSpring.u.condition[0].right_coeff = (v*ff_percent)/100;
+                pSpring.u.condition[0].left_coeff = (v*ff_percent)/100;
+                pSpring.u.condition[1].right_coeff = (v*ff_percent)/100;
+                pSpring.u.condition[1].left_coeff = (v*ff_percent)/100;
+                nprintf(("Joystick", "FF: New handling force = %d\n", (v*ff_percent)/100));
+
+                joy_ff_reload_effect(&pSpring,"Spring");
+                handler_adjust = v;
+        }
+        joy_ff_start_effect(&pSpring,"Spring");
+
+}
+
+void joy_ff_docked()
+{
+        joy_ff_stop_effect(&pDock, "Dock");
+        pDock.u.periodic.magnitude = 40*ff_percent;
+        joy_ff_reload_effect(&pDock, "Dock");
+        joy_ff_start_effect(&pDock, "Dock");
+}
+
+void joy_ff_play_reload_effect()
+{
+        joy_ff_stop_effect(&pDock, "Dock (reload)");
+        pDock.u.periodic.magnitude = 20*ff_percent;
+        joy_ff_reload_effect(&pDock, "Dock (reload)");
+        joy_ff_start_effect(&pDock, "Dock (reload)");
+}
+
+int Joy_ff_afterburning = 0;
+
+void joy_ff_afterburn_on()
+{
+        joy_ff_stop_effect(&pAfterburn1, "Afterburn1");
+        joy_ff_stop_effect(&pAfterburn2, "Afterburn2");
+        pAfterburn1.u.periodic.magnitude = 80*ff_percent;
+        pAfterburn2.u.periodic.magnitude = 44*ff_percent;
+        pAfterburn1.replay.length = -1;
+        pAfterburn2.replay.length = -1;
+        joy_ff_reload_effect(&pAfterburn1, "Afterburn1");
+        joy_ff_reload_effect(&pAfterburn2, "Afterburn2");
+        joy_ff_start_effect(&pAfterburn1, "Afterburn1");
+        joy_ff_start_effect(&pAfterburn2, "Afterburn2");
+ nprintf(("Joystick", "FF: Afterburn started\n"));
+ Joy_ff_afterburning = 1;
+}
+
+void joy_ff_afterburn_off()
+{
+ if (!Joy_ff_afterburning)
+ return;
+        joy_ff_stop_effect(&pAfterburn1, "Afterburn1");
+        joy_ff_stop_effect(&pAfterburn2, "Afterburn2");
+ Joy_ff_afterburning = 0;
+ nprintf(("Joystick", "FF: Afterburn stopped\n"));
+}
+
+void joy_ff_deathroll()
+{
+        if (pExplode.id >= 0) ioctl(fdFFDevice, EVIOCRMFF, pExplode.id);
+        pExplode.id = -1;
+        // TODO: chech if I have to stop the event...
+ joy_ff_start_effect(&pDeathroll1, "Deathroll1");
+ joy_ff_start_effect(&pDeathroll2, "Deathroll2");
+}
+
+void joy_ff_explode()
+{
+ joy_ff_stop_effect(&pDeathroll1, "Deathroll1");
+ joy_ff_stop_effect(&pDeathroll2, "Deathroll2");
+        if (pDeathroll1.id >= 0) ioctl(fdFFDevice, EVIOCRMFF, pDeathroll1.id);
+        if (pDeathroll2.id >= 0) ioctl(fdFFDevice, EVIOCRMFF, pDeathroll2.id);
+        pDeathroll1.id = -1;
+        pDeathroll2.id = -1;
+ joy_ff_stop_effect(&pExplode, "Explode");
+ joy_ff_start_effect(&pExplode, "Explode");
+}
+
+void joy_ff_fly_by(int mag)
+{
+ int gain;
+
+ if (Joy_ff_afterburning)
+ return;
+
+ gain = mag * 120 + 4000;
+ if (gain > 10000)
+ gain = 10000;
+
+        joy_ff_stop_effect(&pAfterburn1, "Afterburn1");
+        joy_ff_stop_effect(&pAfterburn2, "Afterburn2");
+
+        pAfterburn1.u.periodic.magnitude = (gain/100)*ff_percent;
+        pAfterburn2.u.periodic.magnitude = (gain/100)*ff_percent;
+
+        pAfterburn1.replay.length = 6*mag + 400;
+        pAfterburn2.replay.length = 6*mag + 400;
+
+        joy_ff_reload_effect(&pAfterburn1, "Afterburn1 (fly by)");
+        joy_ff_reload_effect(&pAfterburn2, "Afterburn2 (fly by)");
+
+        joy_ff_start_effect(&pAfterburn1, "Afterburn1 (fly by)");
+        joy_ff_start_effect(&pAfterburn2, "Afterburn2 (fly by)");
+}
+
+void joy_reacquire_ff()
+{
+ if (!Joy_ff_enabled)
+ return;
+
+ nprintf(("Joystick", "FF: Reacquiring\n"));
+        // Open device */
+        fdFFDevice = open(device_file_name, O_RDWR);
+        if (fdFFDevice != -1) {
+            mprintf(("Device opened: ", device_file_name));
+     joy_ff_start_effect(&pSpring, "Spring");
+        }
+
+}
+
+void joy_ff_play_dir_effect(float x, float y)
+{
+ int idegs, imag;
+ float degs;
+
+ if (!Joy_ff_enabled)
+ return;
+
+ if (Joy_ff_directional_hit_effect_enabled) {
+ if (x > 8000.0f)
+ x = 8000.0f;
+ else if (x < -8000.0f)
+ x = -8000.0f;
+
+ if (y > 8000.0f)
+ y = 8000.0f;
+ else if (y < -8000.0f)
+ y = -8000.0f;
+
+ imag = (int) fl_sqrt(x * x + y * y);
+ if (imag > 10000)
+ imag = 10000;
+
+ degs = (float)atan2(x, y);
+ idegs = (int) (degs * 18000.0f / PI) + 90;
+ while (idegs < 0)
+ idegs += 36000;
+
+ while (idegs >= 36000)
+ idegs -= 36000;
+
+                pHitEffect1.direction = (idegs*0x10000)/360;
+                pHitEffect1.u.constant.level = (imag*ff_percent)/100;
+                joy_ff_reload_effect(&pHitEffect1,"HitEffect1");
+
+
+ idegs += 9000;
+ if (idegs >= 36000)
+ idegs -= 36000;
+
+                pHitEffect2.direction = (idegs*0x10000)/360;
+                pHitEffect2.u.periodic.magnitude = (imag*ff_percent)/100;
+                joy_ff_reload_effect(&pHitEffect2,"HitEffect2");
+
+ }
+ joy_ff_start_effect(&pHitEffect1, "HitEffect1");
+ joy_ff_start_effect(&pHitEffect2, "HitEffect2");
+ //nprintf(("Joystick", "FF: Dir: %d, Mag = %d\n", idegs, imag));
+}
+
+void joy_ff_play_vector_effect(vec3d *v, float scaler)
+{
+ vec3d vf;
+ float x, y;
+
+ nprintf(("Joystick", "FF: vec = { %f, %f, %f } s = %f\n", v->xyz.x, v->xyz.y, v->xyz.z, scaler));
+ vm_vec_copy_scale(&vf, v, scaler);
+ x = vf.xyz.x;
+ vf.xyz.x = 0.0f;
+
+ if (vf.xyz.y + vf.xyz.z < 0)
+ y = -vm_vec_mag(&vf);
+ else
+ y = vm_vec_mag(&vf);
+
+ joy_ff_play_dir_effect(-x, -y);
+}
+
+
+void joy_ff_play_secondary_shoot(int gain)
+{
+ if (!Joy_ff_enabled)
+ return;
+
+ gain = gain * 100 + 2500;
+ if (gain > 10000)
+ gain = 10000;
+
+        pSecShootEffect.u.constant.level = (gain*ff_percent)/100;
+        pSecShootEffect.replay.length = (150000 + gain * 25)/1000;
+ joy_ff_stop_effect(&pSecShootEffect, "SecShootEffect");
+ joy_ff_reload_effect(&pSecShootEffect, "SecShootEffect");
+ joy_ff_start_effect(&pSecShootEffect, "SecShootEffect");
+}
+
+static int primary_ff_level = 0;
+
+void joy_ff_play_primary_shoot(int gain)
+{
+ if (!Joy_ff_enabled)
+ return;
+
+ if (gain > 10000)
+ gain = 10000;
+
+ if (gain != primary_ff_level) {
+                pShootEffect.u.periodic.magnitude = (gain*ff_percent)/100;
+ primary_ff_level = gain;
+         joy_ff_reload_effect(&pShootEffect, "ShootEffect");
+ }
+ joy_ff_stop_effect(&pShootEffect, "ShootEffect");
+ joy_ff_start_effect(&pShootEffect, "ShootEffect");
+}
+
+#endif          // LINUX_FORCE_FEEDBACK
 #endif // Goober5000 - #ifndef WIN32

I'd be happy if an input person could look over this and tell me what to change for this to get included in some next release.

For the others: Test and have fun! (I am ;) )

Cheers,
SiriusGrey
Title: Re: [PATCH] Force Feedback Support for Linux
Post by: SiriusGrey on December 31, 2009, 05:19:33 am
As an aside: SDL 1.3 will support Force Feedback as well, so if/when FS2SCP switches to SDL1.3 I'd be willing to also write a cross-platform FF backend (does FS2SCP support Mac?)
Cheers,
Johannes
Title: Re: [PATCH] Force Feedback Support for Linux
Post by: Jeff Vader on December 31, 2009, 05:24:27 am
does FS2SCP support Mac?
Yes.
Title: Re: [PATCH] Force Feedback Support for Linux
Post by: chief1983 on December 31, 2009, 01:59:35 pm
Awesome sauce, I love when new users come in with code already written. We'll definitely have to look at this but we do have a lot of stuff going on right now.  I won't let it get forgotten though.
Title: Re: [PATCH] Force Feedback Support for Linux
Post by: SiriusGrey on January 02, 2010, 12:38:22 pm
Thanks :)

Concerning device discovery: Since my joystick mysteriously changed from event10 to event4 I now added device discovery. The new patch is:

Code: [Select]
--- src_fs2_open_svn/fs2_open/code/io/joy-unix.cpp 2009-12-30 22:41:14.000000000 +0100
+++ src_fs2_open_3_6_10/code/io/joy-unix.cpp 2010-01-01 23:54:21.000000000 +0100
@@ -9,6 +9,8 @@
 
 #ifndef WIN32 // Goober5000
 
+#define LINUX_FORCE_FEEDBACK
+
 #include "globalincs/pstypes.h"
 #include "io/joy.h"
 #include "math/fix.h"
@@ -64,6 +66,7 @@
  sdljoy = NULL;
 
  SDL_QuitSubSystem (SDL_INIT_JOYSTICK);
+        joy_ff_shutdown();
 }
 
 void joy_get_caps (int max)
@@ -537,6 +540,7 @@
  SDL_EventState( SDL_JOYHATMOTION, SDL_ENABLE );
 
  Joy_inited = 1;
+        joy_ff_init();
 
  return joy_num_sticks;
 }
@@ -572,6 +576,14 @@
  return 1;
 }
 
+
+#ifndef LINUX_FORCE_FEEDBACK
+
+void joy_ff_shutdown()
+{
+// STUB_FUNCTION;
+}
+
 void joy_ff_adjust_handling(int speed)
 {
 // STUB_FUNCTION;
@@ -632,7 +644,7 @@
 // STUB_FUNCTION;
 }
 
-void joy_ff_play_vector_effect(vec3d *v, float scaler)
+void joy_ff_play_vector_effect(vec3d *v, float scaler);
 {
 // STUB_FUNCTION;
 }
@@ -642,4 +654,619 @@
  joy_ff_afterburn_off();
 }
 
+
+#else // LINUX_FORCE_FEEDBACK
+
+#include "math/vecmat.h"
+#include <stdio.h>
+#include <linux/input.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+
+int joy_ff_handling_scaler;
+int Joy_ff_enabled = 0;
+int Joy_ff_directional_hit_effect_enabled = 1;
+const int ff_percent = 250; // max 327
+
+int fdFFDevice;
+int nMaxEffects;
+
+int joy_ff_create_effects();
+void joy_ff_stop_effects();
+
+ff_effect pHitEffect1;
+ff_effect pHitEffect2;
+ff_effect pShootEffect;
+ff_effect pSecShootEffect;
+ff_effect pSpring;
+ff_effect pAfterburn1;
+ff_effect pAfterburn2;
+ff_effect pDock;
+ff_effect pDeathroll1;
+ff_effect pDeathroll2;
+ff_effect pExplode;
+ff_effect * ff_all_effects[] = {&pHitEffect1,&pHitEffect2,
+                &pShootEffect,&pSecShootEffect,&pSpring,
+                &pAfterburn1,&pAfterburn2,&pDock,
+                &pDeathroll1,&pDeathroll2,&pExplode,0};
+
+const char * device_file_names[] = { "/dev/input/event0",
+                                    "/dev/input/event1",
+                                    "/dev/input/event2",
+                                    "/dev/input/event3",
+                                    "/dev/input/event4",
+                                    "/dev/input/event5",
+                                    "/dev/input/event6",
+                                    "/dev/input/event7",
+                                    "/dev/input/event8",
+                                    "/dev/input/event9",
+                                    "/dev/input/event10",
+                                    0};
+const char * device_file_name;
+
+void joy_ff_afterburn_off();
+
+#define BITS_PER_LONG (sizeof(long) * 8)
+#define OFF(x)  ((x)%BITS_PER_LONG)
+#define BIT(x)  (1UL<<OFF(x))
+#define LONG(x) ((x)/BITS_PER_LONG)
+#define test_bit(bit, array)    ((array[LONG(bit)] >> OFF(bit)) & 1)
+
+int joy_ff_init()
+{
+        int ff_enabled;
+ Joy_ff_enabled = 0; // Assume no force feedback
+        nMaxEffects = 0;
+        mprintf(("Trying to open FF joysticks\n"));
+
+        int device_fn_id = 0;
+        for (;device_file_names[device_fn_id] != 0; device_fn_id++) {
+
+                // Open device */
+                fdFFDevice = open(device_file_names[device_fn_id], O_RDWR);
+                if (fdFFDevice != -1) {
+                    ff_enabled = 1;
+                    mprintf(("Device %s opened\n", device_file_names[device_fn_id]));
+                }
+
+                mprintf(("Trying to open FF joystick - querying features!\n"));
+                /* Query device */
+                unsigned long features[4];
+                if (ioctl(fdFFDevice, EVIOCGBIT(EV_FF, sizeof(unsigned long) * 4), features) == -1) {
+                    perror("Ioctl query");
+                    close(fdFFDevice);
+                    continue;
+                }
+
+                if (!(test_bit(ABS_X, features) && test_bit(ABS_Y, features))) {
+                    perror("No Axis!");
+                    close(fdFFDevice);
+                    continue;
+                }
+
+                if (test_bit(FF_CONSTANT, features)) {
+                    nprintf(("Constant "));};
+                if (test_bit(FF_PERIODIC, features)) {
+                    nprintf(("Periodic "));};
+                if (test_bit(FF_SPRING, features)) {
+                    nprintf(("Spring "));
+                };
+
+                ioctl(fdFFDevice, EVIOCGEFFECTS, &nMaxEffects);
+                for (int i = 0; i < nMaxEffects; i++) ioctl(fdFFDevice, EVIOCRMFF, i);
+                if (joy_ff_create_effects()) {
+                    close(fdFFDevice);
+                    continue;
+                }
+                device_file_name = device_file_names[device_fn_id];
+                Joy_ff_enabled = 1;
+
+                return 0;
+        }
+        mprintf(("No suitable force-feedback device found :(\n"));
+        return -1;
+}
+
+void joy_ff_shutdown()
+{
+ if (Joy_ff_enabled) {
+ joy_ff_stop_effects();
+                for (int i = 0; i < nMaxEffects; i++) ioctl(fdFFDevice, EVIOCRMFF, i);
+                close(fdFFDevice);
+ }
+}
+
+int joy_ff_handle_error(int ec, char *eff_name = NULL)
+{
+ if (ec != 0) {
+ if (eff_name)
+ mprintf(("Joystick", "FF: Error for %s: %i\n", eff_name, ec));
+ else
+ mprintf(("Joystick", "FF: Error: %i\n", ec));
+ }
+        //TODO: handle loss of connection
+ return ec;
+}
+
+
+void joy_ff_reload_effect(ff_effect *eff, const char *name)
+{
+        mprintf(("Joystick", "FF: Reloading effect %s\n", name));
+        if (eff->id >= 0) ioctl(fdFFDevice, EVIOCRMFF, eff->id);
+        eff->id = -1;
+        ioctl(fdFFDevice, EVIOCSFF, eff);
+        //mprintf(("Effect %s has ID %i\n",name,eff->id));
+/*        for (int i = 0; ff_all_effects[i] != 0; i++) {
+            if ((ff_all_effects[i]->id == eff->id) && (eff != ff_all_effects[i])) {
+                printf("WARNING: Deleting overlapping event %i!\n",i);
+                ioctl(fdFFDevice, EVIOCRMFF, i);
+                ff_all_effects[i]->id = -1;
+            }
+        }*/
+}
+
+void joy_ff_start_effect(ff_effect *eff, const char *name)
+{
+        mprintf(("Joystick", "FF: Starting effect %s\n", name));
+        if (eff->id == -1) joy_ff_reload_effect(eff,name);
+        if (eff->id == -1) {
+            perror("Not playing effect - Upload failed.");
+            return;
+        }
+        struct input_event play;
+        play.type = EV_FF;
+        play.code = eff->id;
+        play.value = 1;
+        write(fdFFDevice, (const void*) &play, sizeof(play));
+}
+
+void joy_ff_stop_effect(ff_effect *eff, const char *name)
+{
+        if (eff->id == -1) {
+            return;
+        }
+ mprintf(("Joystick", "FF: Stopping effect %s\n", name));
+        struct input_event play;
+        play.type = EV_FF;
+        play.code = eff->id;
+        play.value = 0;
+        write(fdFFDevice, (const void*) &play, sizeof(play));
+}
+
+int joy_ff_create_effects()
+{
+
+
+        pHitEffect1.type = FF_CONSTANT;
+        pHitEffect1.id = -1;
+        pHitEffect1.u.constant.level = 100*ff_percent;
+        pHitEffect1.direction = 0;
+        pHitEffect1.u.constant.envelope.attack_length = 0;
+        pHitEffect1.u.constant.envelope.attack_level = 100*ff_percent;
+        pHitEffect1.u.constant.envelope.fade_length = 120;
+        pHitEffect1.u.constant.envelope.fade_level = 1;
+        pHitEffect1.trigger.button = 0;
+        pHitEffect1.trigger.interval = 0;
+        pHitEffect1.replay.length = 300;
+        pHitEffect1.replay.delay = 0;
+
+        pHitEffect2.type = FF_PERIODIC;
+        pHitEffect2.id = -1;
+        pHitEffect2.u.periodic.waveform = FF_SINE;
+        pHitEffect2.u.periodic.period = 100;       /* 0.1 second */
+        pHitEffect2.u.periodic.magnitude = 100*ff_percent; 
+        pHitEffect2.u.periodic.offset = 0;
+        pHitEffect2.u.periodic.phase = 0;
+        pHitEffect2.direction = 0x4000;  /* Along X axis */
+        pHitEffect2.u.periodic.envelope.attack_length = 100;
+        pHitEffect2.u.periodic.envelope.attack_level = 0;
+        pHitEffect2.u.periodic.envelope.fade_length = 100;
+        pHitEffect2.u.periodic.envelope.fade_level = 0;
+        pHitEffect2.trigger.button = 0;
+        pHitEffect2.trigger.interval = 0;
+        pHitEffect2.replay.length = 300;  /* ms */
+        pHitEffect2.replay.delay = 0;
+
+        pShootEffect.type = FF_PERIODIC;
+        pShootEffect.id = -1;
+        pShootEffect.u.periodic.waveform = FF_SAW_DOWN;
+        pShootEffect.u.periodic.period = 20;       /* ms */
+        pShootEffect.u.periodic.magnitude = 100*ff_percent; 
+        pShootEffect.u.periodic.offset = 0;
+        pShootEffect.u.periodic.phase = 0;
+        pShootEffect.direction = 0;
+        pShootEffect.u.periodic.envelope.attack_length = 0;
+        pShootEffect.u.periodic.envelope.attack_level = 0;
+        pShootEffect.u.periodic.envelope.fade_length = 120;
+        pShootEffect.u.periodic.envelope.fade_level = 0;
+        pShootEffect.trigger.button = 0;
+        pShootEffect.trigger.interval = 0;
+        pShootEffect.replay.length = 160;  /* ms */
+        pShootEffect.replay.delay = 0;
+
+        pSecShootEffect.type = FF_CONSTANT;
+        pSecShootEffect.id = -1;
+        pSecShootEffect.u.constant.level = 100*ff_percent;
+        pSecShootEffect.direction = 0;
+        pSecShootEffect.u.constant.envelope.attack_length = 50;
+        pSecShootEffect.u.constant.envelope.attack_level = 100*ff_percent;
+        pSecShootEffect.u.constant.envelope.fade_length = 100;
+        pSecShootEffect.u.constant.envelope.fade_level = 1;
+        pSecShootEffect.trigger.button = 0;
+        pSecShootEffect.trigger.interval = 0;
+        pSecShootEffect.replay.length = 200;
+        pSecShootEffect.replay.delay = 0;
+
+        pSpring.type = FF_SPRING;
+        pSpring.id = -1;
+        pSpring.u.condition[0].right_saturation = 0x7fff;
+        pSpring.u.condition[0].left_saturation = 0x7fff;
+        pSpring.u.condition[0].right_coeff = 0x7fff;
+        pSpring.u.condition[0].left_coeff = 0x7fff;
+        pSpring.u.condition[0].deadband = 0x0;
+        pSpring.u.condition[0].center = 0x0;
+        pSpring.u.condition[1] = pSpring.u.condition[0];
+        pSpring.trigger.button = 0;
+        pSpring.trigger.interval = 0;
+        pSpring.replay.length = -1;  /* 20 seconds */
+        pSpring.replay.delay = 0;
+
+        pAfterburn1.type = FF_PERIODIC;
+        pAfterburn1.id = -1;
+        pAfterburn1.u.periodic.waveform = FF_SINE;
+        pAfterburn1.u.periodic.period = 20;       /* ms */
+        pAfterburn1.u.periodic.magnitude = 80*ff_percent;
+        pAfterburn1.u.periodic.offset = 0;
+        pAfterburn1.u.periodic.phase = 0;
+        pAfterburn1.direction = 0;
+        pAfterburn1.u.periodic.envelope.attack_length = 0;
+        pAfterburn1.u.periodic.envelope.attack_level = 0;
+        pAfterburn1.u.periodic.envelope.fade_length = 0;
+        pAfterburn1.u.periodic.envelope.fade_level = 0;
+        pAfterburn1.trigger.button = 0;
+        pAfterburn1.trigger.interval = 0;
+        pAfterburn1.replay.length = -1;  /* ms */
+        pAfterburn1.replay.delay = 0;
+
+        pAfterburn2.type = FF_PERIODIC;
+        pAfterburn2.id = -1;
+        pAfterburn2.u.periodic.waveform = FF_SINE;
+        pAfterburn2.u.periodic.period = 120;       /* ms */
+        pAfterburn2.u.periodic.magnitude = 44*ff_percent; 
+        pAfterburn2.u.periodic.offset = 0;
+        pAfterburn2.u.periodic.phase = 0;
+        pAfterburn2.direction = 0x4000;
+        pAfterburn2.u.periodic.envelope.attack_length = 0;
+        pAfterburn2.u.periodic.envelope.attack_level = 0;
+        pAfterburn2.u.periodic.envelope.fade_length = 0;
+        pAfterburn2.u.periodic.envelope.fade_level = 0;
+        pAfterburn2.trigger.button = 0;
+        pAfterburn2.trigger.interval = 0;
+        pAfterburn2.replay.length = -1;  /* ms */
+        pAfterburn2.replay.delay = 0;
+
+        pDock.type = FF_PERIODIC;
+        pDock.id = -1;
+        pDock.u.periodic.waveform = FF_SQUARE;
+        pDock.u.periodic.period = 100;       /* ms */
+        pDock.u.periodic.magnitude = 40*ff_percent; 
+        pDock.u.periodic.offset = 0;
+        pDock.u.periodic.phase = 0;
+        pDock.direction = 0x4000;
+        pDock.u.periodic.envelope.attack_length = 0;
+        pDock.u.periodic.envelope.attack_level = 0;
+        pDock.u.periodic.envelope.fade_length = 0;
+        pDock.u.periodic.envelope.fade_level = 0;
+        pDock.trigger.button = 0;
+        pDock.trigger.interval = 0;
+        pDock.replay.length = 125;  /* ms */
+        pDock.replay.delay = 0;
+
+        pExplode.type = FF_PERIODIC;
+        pExplode.id = -1;
+        pExplode.u.periodic.waveform = FF_SAW_DOWN;
+        //pExplode.u.periodic.period = 20;       /* ms */
+        pExplode.u.periodic.period = 50;       /* ms - 50 is better :) */
+        pExplode.u.periodic.magnitude = 100*ff_percent; 
+        pExplode.u.periodic.offset = 0;
+        pExplode.u.periodic.phase = 0;
+        pExplode.direction = 0x4000;
+        pExplode.u.periodic.envelope.attack_length = 0;
+        pExplode.u.periodic.envelope.attack_level = 0;
+        pExplode.u.periodic.envelope.fade_length = 500;
+        pExplode.u.periodic.envelope.fade_level = 0;
+        pExplode.trigger.button = 0;
+        pExplode.trigger.interval = 0;
+        pExplode.replay.length = 500;  /* ms */
+        pExplode.replay.delay = 0;
+
+        pDeathroll1.type = FF_PERIODIC;
+        pDeathroll1.id = -1;
+        pDeathroll1.u.periodic.waveform = FF_SINE;
+        pDeathroll1.u.periodic.period = 200;       /* ms */
+        pDeathroll1.u.periodic.magnitude = 100*ff_percent; 
+        pDeathroll1.u.periodic.offset = 0;
+        pDeathroll1.u.periodic.phase = 0x2000;
+        pDeathroll1.direction = 0;
+        pDeathroll1.u.periodic.envelope.attack_length = 2000;
+        pDeathroll1.u.periodic.envelope.attack_level = 0;
+        pDeathroll1.u.periodic.envelope.fade_length = 0;
+        pDeathroll1.u.periodic.envelope.fade_level = 0;
+        pDeathroll1.trigger.button = 0;
+        pDeathroll1.trigger.interval = 0;
+        pDeathroll1.replay.length = -1;  /* ms */
+        pDeathroll1.replay.delay = 0;
+
+        pDeathroll2.type = FF_PERIODIC;
+        pDeathroll2.id = -1;
+        pDeathroll2.u.periodic.waveform = FF_SINE;
+        pDeathroll2.u.periodic.period = 200;       /* ms */
+        pDeathroll2.u.periodic.magnitude = 100*ff_percent; 
+        pDeathroll2.u.periodic.offset = 0;
+        pDeathroll2.u.periodic.phase = 0;
+        pDeathroll2.direction = 0x4000;
+        pDeathroll2.u.periodic.envelope.attack_length = 2000;
+        pDeathroll2.u.periodic.envelope.attack_level = 0;
+        pDeathroll2.u.periodic.envelope.fade_length = 0;
+        pDeathroll2.u.periodic.envelope.fade_level = 0;
+        pDeathroll2.trigger.button = 0;
+        pDeathroll2.trigger.interval = 0;
+        pDeathroll2.replay.length = -1;  /* ms */
+        pDeathroll2.replay.delay = 0;
+
+ return 0;
+}
+
+void joy_ff_stop_effects()
+{
+ joy_ff_afterburn_off();
+}
+
+void joy_ff_mission_init(vec3d v)
+{
+ v.xyz.z = 0.0f;
+// joy_ff_handling_scaler = (int) ((vm_vec_mag(&v) - 1.3f) * 10.5f);
+ joy_ff_handling_scaler = (int) ((vm_vec_mag(&v) + 1.3f) * 5.0f);
+// joy_ff_handling_scaler = (int) (vm_vec_mag(&v) * 7.5f);
+}
+
+int handler_adjust = -1;
+void joy_ff_adjust_handling(int speed)
+{
+ int v;
+
+ v = speed * joy_ff_handling_scaler * 2 / 3;
+// v += joy_ff_handling_scaler * joy_ff_handling_scaler * 6 / 7 + 250;
+ v += joy_ff_handling_scaler * 45 - 500;
+ if (v > 10000)
+ v = 10000;
+
+        if (handler_adjust != v) {
+                pSpring.u.condition[0].right_coeff = (v*ff_percent)/100;
+                pSpring.u.condition[0].left_coeff = (v*ff_percent)/100;
+                pSpring.u.condition[1].right_coeff = (v*ff_percent)/100;
+                pSpring.u.condition[1].left_coeff = (v*ff_percent)/100;
+                mprintf(("Joystick", "FF: New handling force = %d\n", (v*ff_percent)/100));
+
+                joy_ff_reload_effect(&pSpring,"Spring");
+                handler_adjust = v;
+        }
+        joy_ff_start_effect(&pSpring,"Spring");
+
+}
+
+void joy_ff_docked()
+{
+        joy_ff_stop_effect(&pDock, "Dock");
+        pDock.u.periodic.magnitude = 40*ff_percent;
+        joy_ff_reload_effect(&pDock, "Dock");
+        joy_ff_start_effect(&pDock, "Dock");
+}
+
+void joy_ff_play_reload_effect()
+{
+        joy_ff_stop_effect(&pDock, "Dock (reload)");
+        pDock.u.periodic.magnitude = 20*ff_percent;
+        joy_ff_reload_effect(&pDock, "Dock (reload)");
+        joy_ff_start_effect(&pDock, "Dock (reload)");
+}
+
+int Joy_ff_afterburning = 0;
+
+void joy_ff_afterburn_on()
+{
+        joy_ff_stop_effect(&pAfterburn1, "Afterburn1");
+        joy_ff_stop_effect(&pAfterburn2, "Afterburn2");
+        pAfterburn1.u.periodic.magnitude = 80*ff_percent;
+        pAfterburn2.u.periodic.magnitude = 44*ff_percent;
+        pAfterburn1.replay.length = -1;
+        pAfterburn2.replay.length = -1;
+        joy_ff_reload_effect(&pAfterburn1, "Afterburn1");
+        joy_ff_reload_effect(&pAfterburn2, "Afterburn2");
+        joy_ff_start_effect(&pAfterburn1, "Afterburn1");
+        joy_ff_start_effect(&pAfterburn2, "Afterburn2");
+ mprintf(("Joystick", "FF: Afterburn started\n"));
+ Joy_ff_afterburning = 1;
+}
+
+void joy_ff_afterburn_off()
+{
+ if (!Joy_ff_afterburning)
+ return;
+        joy_ff_stop_effect(&pAfterburn1, "Afterburn1");
+        joy_ff_stop_effect(&pAfterburn2, "Afterburn2");
+ Joy_ff_afterburning = 0;
+ mprintf(("Joystick", "FF: Afterburn stopped\n"));
+}
+
+void joy_ff_deathroll()
+{
+        if (pExplode.id >= 0) ioctl(fdFFDevice, EVIOCRMFF, pExplode.id);
+        pExplode.id = -1;
+        // TODO: chech if I have to stop the event...
+ joy_ff_start_effect(&pDeathroll1, "Deathroll1");
+ joy_ff_start_effect(&pDeathroll2, "Deathroll2");
+}
+
+void joy_ff_explode()
+{
+ joy_ff_stop_effect(&pDeathroll1, "Deathroll1");
+ joy_ff_stop_effect(&pDeathroll2, "Deathroll2");
+        if (pDeathroll1.id >= 0) ioctl(fdFFDevice, EVIOCRMFF, pDeathroll1.id);
+        if (pDeathroll2.id >= 0) ioctl(fdFFDevice, EVIOCRMFF, pDeathroll2.id);
+        pDeathroll1.id = -1;
+        pDeathroll2.id = -1;
+ joy_ff_stop_effect(&pExplode, "Explode");
+ joy_ff_start_effect(&pExplode, "Explode");
+}
+
+void joy_ff_fly_by(int mag)
+{
+ int gain;
+
+ if (Joy_ff_afterburning)
+ return;
+
+ gain = mag * 120 + 4000;
+ if (gain > 10000)
+ gain = 10000;
+
+        joy_ff_stop_effect(&pAfterburn1, "Afterburn1");
+        joy_ff_stop_effect(&pAfterburn2, "Afterburn2");
+
+        pAfterburn1.u.periodic.magnitude = (gain/100)*ff_percent;
+        pAfterburn2.u.periodic.magnitude = (gain/100)*ff_percent;
+
+        pAfterburn1.replay.length = 6*mag + 400;
+        pAfterburn2.replay.length = 6*mag + 400;
+
+        joy_ff_reload_effect(&pAfterburn1, "Afterburn1 (fly by)");
+        joy_ff_reload_effect(&pAfterburn2, "Afterburn2 (fly by)");
+
+        joy_ff_start_effect(&pAfterburn1, "Afterburn1 (fly by)");
+        joy_ff_start_effect(&pAfterburn2, "Afterburn2 (fly by)");
+}
+
+void joy_reacquire_ff()
+{
+ if (!Joy_ff_enabled)
+ return;
+
+ mprintf(("Joystick", "FF: Reacquiring\n"));
+        // Open device */
+        fdFFDevice = open(device_file_name, O_RDWR);
+        if (fdFFDevice != -1) {
+            mprintf(("Device opened: ", device_file_name));
+     joy_ff_start_effect(&pSpring, "Spring");
+        }
+
+}
+
+void joy_ff_play_dir_effect(float x, float y)
+{
+ int idegs, imag;
+ float degs;
+
+ if (!Joy_ff_enabled)
+ return;
+
+ if (Joy_ff_directional_hit_effect_enabled) {
+ if (x > 8000.0f)
+ x = 8000.0f;
+ else if (x < -8000.0f)
+ x = -8000.0f;
+
+ if (y > 8000.0f)
+ y = 8000.0f;
+ else if (y < -8000.0f)
+ y = -8000.0f;
+
+ imag = (int) fl_sqrt(x * x + y * y);
+ if (imag > 10000)
+ imag = 10000;
+
+ degs = (float)atan2(x, y);
+ idegs = (int) (degs * 18000.0f / PI) + 90;
+ while (idegs < 0)
+ idegs += 36000;
+
+ while (idegs >= 36000)
+ idegs -= 36000;
+
+                pHitEffect1.direction = (idegs*0x10000)/360;
+                pHitEffect1.u.constant.level = (imag*ff_percent)/100;
+                joy_ff_reload_effect(&pHitEffect1,"HitEffect1");
+
+
+ idegs += 9000;
+ if (idegs >= 36000)
+ idegs -= 36000;
+
+                pHitEffect2.direction = (idegs*0x10000)/360;
+                pHitEffect2.u.periodic.magnitude = (imag*ff_percent)/100;
+                joy_ff_reload_effect(&pHitEffect2,"HitEffect2");
+
+ }
+ joy_ff_start_effect(&pHitEffect1, "HitEffect1");
+ joy_ff_start_effect(&pHitEffect2, "HitEffect2");
+ //nprintf(("Joystick", "FF: Dir: %d, Mag = %d\n", idegs, imag));
+}
+
+void joy_ff_play_vector_effect(vec3d *v, float scaler)
+{
+ vec3d vf;
+ float x, y;
+
+ //nprintf(("Joystick", "FF: vec = { %f, %f, %f } s = %f\n", v->xyz.x, v->xyz.y, v->xyz.z, scaler));
+ vm_vec_copy_scale(&vf, v, scaler);
+ x = vf.xyz.x;
+ vf.xyz.x = 0.0f;
+
+ if (vf.xyz.y + vf.xyz.z < 0)
+ y = -vm_vec_mag(&vf);
+ else
+ y = vm_vec_mag(&vf);
+
+ joy_ff_play_dir_effect(-x, -y);
+}
+
+
+void joy_ff_play_secondary_shoot(int gain)
+{
+ if (!Joy_ff_enabled)
+ return;
+
+ gain = gain * 100 + 2500;
+ if (gain > 10000)
+ gain = 10000;
+
+        pSecShootEffect.u.constant.level = (gain*ff_percent)/100;
+        pSecShootEffect.replay.length = (150000 + gain * 25)/1000;
+ joy_ff_stop_effect(&pSecShootEffect, "SecShootEffect");
+ joy_ff_reload_effect(&pSecShootEffect, "SecShootEffect");
+ joy_ff_start_effect(&pSecShootEffect, "SecShootEffect");
+}
+
+static int primary_ff_level = 0;
+
+void joy_ff_play_primary_shoot(int gain)
+{
+ if (!Joy_ff_enabled)
+ return;
+
+ if (gain > 10000)
+ gain = 10000;
+
+ if (gain != primary_ff_level) {
+                pShootEffect.u.periodic.magnitude = (gain*ff_percent)/100;
+ primary_ff_level = gain;
+         joy_ff_reload_effect(&pShootEffect, "ShootEffect");
+ }
+ joy_ff_stop_effect(&pShootEffect, "ShootEffect");
+ joy_ff_start_effect(&pShootEffect, "ShootEffect");
+}
+
+#endif          // LINUX_FORCE_FEEDBACK
 #endif // Goober5000 - #ifndef WIN32

This code picks the first force-feedback capable device - unfortunately it is currently not (easily) possible to associate it with an input device, since that is managed by the STL. However, i think it is rare to have more that one FF device plugged in at the same time, so until SDL 1.3 arrives this should suffice.

Keep up the good work - I am looking forward to the next version!

Cheers,
SiriusGrey