Author Topic: [PATCH] Force Feedback Support for Linux  (Read 1425 times)

0 Members and 1 Guest are viewing this topic.

[PATCH] Force Feedback Support for Linux
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

 
Re: [PATCH] Force Feedback Support for Linux
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

 

Offline Jeff Vader

  • The Back of the Hero!
  • 212
  • Bwahaha
    • Twitter
Re: [PATCH] Force Feedback Support for Linux
14:08 < achillion > there's too much talk of butts and dongs in here
14:08 < achillion > the level of discourse has really plummeted
14:08 < achillion > Let's talk about politics instead
14:08 <@The_E > butts and dongs are part of #hard-light's brand now
14:08 <@The_E > well
14:08 <@The_E > EvilBagel's brand, at least

01:06 < T-Rog > welp
01:07 < T-Rog > I've got to take some very strong antibiotics
01:07 < achillion > penis infection?
01:08 < T-Rog > Chlamydia
01:08 < achillion > O.o
01:09 < achillion > well
01:09 < achillion > I guess that happens
01:09 < T-Rog > at least it's curable
01:09 < achillion > yeah
01:10 < T-Rog > I take it you weren't actually expecting it to be a penis infection
01:10 < achillion > I was not

14:04 < achillion > Sometimes the way to simplify is to just have a habit and not think about it too much
14:05 < achillion > until stuff explodes
14:05 < achillion > then you start thinking about it

22:16 < T-Rog > I don't know how my gf would feel about Jewish conspiracy porn

15:41 <-INFO > EveningTea [[email protected]] has joined #hard-light
15:47 < EvilBagel> butt
15:51 < Achillion> yes
15:53 <-INFO > EveningTea [[email protected]] has quit [Quit: http://www.mibbit.com ajax IRC Client]

18:53 < Achillion> Dicks are fun

21:41 < MatthTheGeek> you can't spell assassin without two asses

20:05 < sigtau> i'm mining titcoins from now on

00:31 < oldlaptop> Drunken antisocial educated freezing hicks with good Internet == Finland stereotype

11:46 <-INFO > Kobrar [[email protected]] has joined #hard-light
11:50 < achtung> Surely you've heard of DVDA
11:50 < achtung> Double Vaginal Double ANal
11:51 < Kobrar> ...
11:51 <-INFO > Kobrar [[email protected]] has left #hard-light []

 

Offline chief1983

  • Still lacks a custom title
  • Moderator
  • 212
  • ⬇️⬆️⬅️⬅️À➡️⬇️
    • Minecraft
    • Skype
    • Steam
    • Twitter
    • Fate of the Galaxy
Re: [PATCH] Force Feedback Support for Linux
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.
Fate of the Galaxy - Now Hiring!  Apply within | Diaspora | SCP Home | Collada Importer for PCS2
Karajorma's 'How to report bugs' | Mantis
#freespace | #scp-swc | #diaspora | #SCP | #hard-light on EsperNet

"You may not sell or otherwise commercially exploit the source or things you created based on the source." -- Excerpt from FSO license, for reference

Nuclear1:  Jesus Christ zack you're a little too hamyurger for HLP right now...
iamzack:  i dont have hamynerge i just want ptatoc hips D:
redsniper:  Platonic hips?!
iamzack:  lays

 
Re: [PATCH] Force Feedback Support for Linux
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