Jump to content
Search In
  • More options...
Find results that contain...
Find results in...
xttl

mouselook/freelook for vanilla Heretic, Hexen and Strife

Recommended Posts

Ever wanted to play vanilla Heretic or Hexen with mouselook or freelook like in ZDoom and other advanced ports? Well now you can!

First, disable internal mouse support from Setup (set your controller to keyboard only). Then, download this little program, place it into the same directory with your vanilla game executable file and run it.

Sensitivity and button mappings can be adjusted with command line parameters, use ravmouse -? to see how.

It works fine in demos and netgames even if the other players do not have ravmouse. It should also work fine with Heretic+, Hexen+, heretic.com and any other EXE hacks you might be using since it doesn't modify the executable at all.

Tested in DOSBox only so far but I see no reason why it wouldn't work on real hardware.

Source code (for 16-bit Open Watcom) here if you're interested.

EXE hack for Strife, this one does not support demos or multiplayer...

Share this post


Link to post

I expected slow keyboard-like handling, but I was pleasantly surprised.

Since decent mouse look is possible with the original executables, maybe Chocolate Heretic and Hexen (and Strife?) could have this option.

Share this post


Link to post

It uses this: http://doomwiki.org/wiki/External_control_driver

Because the external input driver API in Heretic and Hexen was intended to be used (among other things) by VR hardware with head tracking, it allowss setting the view pitch and yaw in the game. Since vertical aiming is always locked (when autoaim doesn't kick in, anyway) to the orientation of the camera, or your head, setting the pitch field is actually also useful for implementing real functional mouselook in vanilla even though this'd be considered a limitation in real VR use.

I don't know if this is possible to do in Strife, it definitely has support for external control drivers but since it isn't open source, I don't know the buffer format and whether it allows for something like this. If it is possible, I'll add support for Strife later. nope, but have a hacked EXE instead

I may also improve the program otherwise, would be handy to have the settings in a configuration file at least, perhaps integrate the functionality of Heretic/Hexen+ or heretic.com.

Share this post


Link to post

Edit: Disregard, this was a problem with how I setup dosbox. Neat stuff!

Neat, I got horizontal mouse movement and the mappings working, but I can't seem to get vertical mouselook to work. I created a new hexen.cfg and set it to keyboard only and tried playing with -vs values.. no dice though.

Share this post


Link to post

Turns out it is not possible to use an external input driver to do this in Strife, so here is a hacked EXE for that instead. This one will break demos and multiplayer, but since Strife does not support coop, nobody plays Strife deathmatch and vanilla Strife demo recording is broken in SP anyway I don't think it matters.

Just run that instead of strife1.exe, no way to adjust vertical sensitivity separately from horizontal, at least not yet but I hope the hardcoded divisor feels ok.

Share this post


Link to post

Will you do a little write up about your findings before you forget the specifics?

I'm talking about the strife hacked exe, since you provided the code for the tool. Also, thanks and a job well done !! I will test it out for myself in the next days.

Share this post


Link to post

Ok. The patch for Strife is actually quite simple, it works like this:

First, as in all games using the Doom engine, there is a function called G_BuildTiccmd. This function builds ticcmds based on input from keyboard, mouse, joystick, or an external control driver. As you may already know, these ticcmds are what get sent over the network in multiplayer games and (partially) recorded in demo files. Unlike Heretic and Hexen, Strife's ticcmds do not support fine control over how much the player is turning up/down on any given gametic. Instead, there are only 3 extended button flags (0/1 toggles) provided for this. (Chocolate Strife calls them BT2_CENTERVIEW, BT2_LOOKUP and BT2_LOOKDOWN)

Anyway, inside this same function, global variable mousey (set by G_Responder) is used for adjusting forwards/backwards movement for the current ticcmd. Basically, the patch works by changing "forward += mousey;" to:

players[0].pitch += mousey >> 3;
if (players[0].pitch > LOOKUPMAX)
  players[0].pitch = LOOKUPMAX;
else if (players[0].pitch < LOOKDOWNMAX)
  players[0].pitch = LOOKDOWNMAX;
Since this bypasses the ticcmd system and just writes to player #1's player_t structure directly it won't work over the network or get recorded in demos. (However, on the other hand, it actually allows for slightly finer control over looking up/down than is possible in Heretic and Hexen via ticcmds!)

I also removed this part
    if (cmd->buttons2 & BT2_CENTERVIEW)
        player->centerview = 1;

    if (player->centerview)
    {
        if (player->pitch <= 0)
        {
            if (player->pitch < 0)
                player->pitch = player->pitch + CENTERVIEWAMOUNT;
        }
        else
        {
            player->pitch = player->pitch - CENTERVIEWAMOUNT;
        }
        if (abs(player->pitch) < CENTERVIEWAMOUNT)
        {
            player->pitch = 0;
            player->centerview = 0;
        }
    }
from P_MovePlayer completely to free some space for patching the binary equivalent of the first code snippet in. Doing this also handily prevents automatic view centering every time you try to press the run key (or all the time if you're using the always run hack) or fall down from a jump.

Share this post


Link to post

Your links are all broken for me.

Nice hack though. Reminds me that I never got around to updating my Control API library code to support the -externdriver mechanism.

Share this post


Link to post

I updated the driver for Heretic and Hexen a bit. New binary and source. Source code is also available from here in case there's a problem with those links.

 

Spoiler

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <mem.h>
#include <i86.h>
#include <process.h>
#include <dos.h>
#include <malloc.h>
#include <io.h>
#include <stdint.h>

//
// From Heretic / Hexen source code, definition of the structure that an
// external input driver must fill and button action flags.
//

#define EBT_FIRE             1
#define EBT_OPENDOOR         2
#define EBT_SPEED            4
#define EBT_STRAFE           8
#define EBT_MAP              0x10
#define EBT_INVENTORYLEFT    0x20
#define EBT_INVENTORYRIGHT   0x40
#define EBT_USEARTIFACT      0x80
#define EBT_FLYDROP          0x100
#define EBT_CENTERVIEW       0x200
#define EBT_PAUSE            0x400
#define EBT_WEAPONCYCLE      0x800
#define EBT_JUMP             0x1000

typedef _Packed struct
{
  int16_t  vector;              // Interrupt vector
  int8_t   moveForward;         // forward/backward (-50 to 50)
  int8_t   moveSideways;        // strafe (-50 to 50)
  int16_t  angleTurn;           // turning speed (640 [slow] 1280 [fast])
  int16_t  angleHead;           // head angle (+2080 [left] : 0 [center] : -2048 [right])
  int8_t   pitch;               // look up/down (-110 : +90)
  int8_t   flyDirection;        // flyheight (+1/-1)
  uint16_t buttons;             // EBT_* flags
}
externdata_t;

#define MOUSE_INTNUM 0x33
#define MOUSE_RESET 0
#define MOUSE_GETMICKEYS 11
#define MOUSE_GETBUTTONS 3

#define MY_DEFAULT_INTNUM 0x64
#define DEFAULT_HSENS 40
#define DEFAULT_VSENS 1
#define DEFAULT_DCLICKTICS 20

externdata_t extdata;
void interrupt (*saved_vector) (void);
int vector_is_hooked = 0;
union REGPACK regs;

// these can be changed from the config file or command line
int myintnum = MY_DEFAULT_INTNUM;
int h_sensitivity = DEFAULT_HSENS;
int v_sensitivity = DEFAULT_VSENS;
int dclicktics = DEFAULT_DCLICKTICS;

#define NUM_BUTTONS 3
//                                left       right         middle
int clickactions[NUM_BUTTONS]  = {EBT_FIRE, EBT_STRAFE,   0};
int dclickactions[NUM_BUTTONS] = {0,        EBT_OPENDOOR, EBT_CENTERVIEW};

// need to know this to time double clicks properly
int ticdup;

int myargc;
char **myargv;

void Error (char *error, ...)
{
  va_list argptr;

  if (vector_is_hooked)
    _dos_setvect (myintnum, saved_vector);

  printf ("Error: ");

  va_start (argptr,error);
  vprintf (error,argptr);
  va_end (argptr);

  printf ("\n");
  exit(-1);
}

int CheckParmWithArgs(char *parm, int num_args)
{
  int i;

  for (i = 1; i < myargc-num_args; i++)
  {
    if (!stricmp(parm, myargv[i]))
      return i;
  }

  return 0;
}

int CheckParm (char *parm)
{
  return CheckParmWithArgs (parm, 0);
}

int InitMouse (void)
{
  regs.w.ax = MOUSE_RESET;
  intr (MOUSE_INTNUM, &regs);

  if (regs.w.ax == 0xFFFF)
    return 1; // mouse driver installed
  else
    return 0; // not installed
}

void interrupt ControlISR (void)
{
  static int pitch = 0;
  static int dclicktime[NUM_BUTTONS]={0,0,0};
  static int dclickstate[NUM_BUTTONS]={0,0,0};
  static int dclicks[NUM_BUTTONS]={0,0,0};
  int mbuttons[NUM_BUTTONS]={0,0,0};
  int i;
  long int mxmove, mymove;
  
  extdata.buttons = 0;

  regs.w.ax = MOUSE_GETBUTTONS;
  intr (MOUSE_INTNUM, &regs);

  if (regs.w.bx & 1) mbuttons[0] = 1;
  if (regs.w.bx & 2) mbuttons[1] = 1;
  if (regs.w.bx & 4) mbuttons[2] = 1;
  
  for (i=0; i<NUM_BUTTONS; i++)
  {
    if (mbuttons[i])
      extdata.buttons |= clickactions[i];
    
    if (mbuttons[i] != dclickstate[i] && dclicktime[i] > 1)
    {
      dclickstate[i] = mbuttons[i];
      if (dclickstate[i])
        dclicks[i]++;
	
      if (dclicks[i]==2)
      {
        extdata.buttons |= dclickactions[i];
	dclicks[i] = 0;
      }
      else
        dclicktime[i] = 0;
    }
    else
    {
      dclicktime[i] += ticdup;
      if (dclicktime[i] > dclicktics)
      {
        dclicks[i] = 0;
	dclickstate[i] = 0;
      }
    }
  }

  regs.w.ax = MOUSE_GETMICKEYS;
  intr (MOUSE_INTNUM, &regs);

  extdata.angleTurn = regs.w.cx * h_sensitivity;
  
  // must be inverted when not strafing
  if (!(extdata.buttons & EBT_STRAFE))
    extdata.angleTurn = -extdata.angleTurn;
        
  if (extdata.buttons & EBT_CENTERVIEW)
    pitch = 0;
  else
  {
    pitch -= regs.w.dx * v_sensitivity;

    if (pitch < -110)
      pitch = -110;
    else if (pitch > 90)
      pitch = 90;
  }

  extdata.pitch = pitch;

  return;
}

int main (int argc, char **argv)
{
  unsigned long flataddr;
  unsigned char far *vector;
  char *launchcmd;
  char **spawnargs;
  int p;
  
  printf ("\n"\
          "RAVMOUSE - External mouse driver with mouselook for Heretic/Hexen\n"\
          "by xttl@dwf, built on " __DATE__ " " __TIME__ "\n"\
          "\n");

  myargc = argc;
  myargv = argv;

  if (CheckParm("-?"))
  {
    printf ("-rmvec int......: Specify alternate interrupt vector (default 0x%02X).\n", myintnum);
    printf ("-hs/-vs num.....: Change horizontal/vertical sensitivity (defaults %i/%i).\n", h_sensitivity, v_sensitivity);
    printf ("-game/-exe/-exec: Specify game launch command other than \"heretic\"/\"hexen\".\n");
    printf ("-dctics num.....: Change double click timing window (default is %i gametics).\n", dclicktics);
    printf ("-mb#/-db# flags.: Change button single click (-mb#) or double click (-db#)\n"\
            "                  action flags. Left mouse button is #1, right button is #2\n"\
	    "                  and middle button is #3. Defaults are MB1=fire, MB2=strafe,\n"\
            "                  MB3=do nothing, DB1=do nothing, DB2=open/use, and\n"\
	    "                  DB3=center view.\n");

   printf ("\n"\
           "Action flags: 0x0001 = fire              0x0002 = use\n"\
           "              0x0004 = run               0x0008 = strafe\n"\
           "              0x0010 = automap           0x0020 = inventory left\n"\
           "              0x0040 = inventory right   0x0080 = use artifact\n"\
           "              0x0100 = stop flying       0x0200 = center view\n"\
           "              0x0400 = toggle pause      0x0800 = cycle weapons\n"\
           "              0x1000 = jump (Hexen only) \n"\
           " * Flags can be added together if you want to bind multiple actions to a\n"\
           "   single mouse button. For example: -mb3 0x81 = fire AND use artifact\n"\
           "   when you press the middle button.\n");

    return -1;
  }

  if (!InitMouse())
    Error ("Mouse driver not detected.");
    
  // must know this to time double clicks properly
  p = CheckParmWithArgs("-dup", 1);
  if (p)
    sscanf(myargv[p+1], "%i", &ticdup);
  else
    ticdup = 1;
    
  p = CheckParmWithArgs("-dctics", 1);
  if (p)
    dclicktics = atoi(myargv[p+1]);

  p = CheckParmWithArgs("-rmvec", 1);
  if (p)
    sscanf(myargv[p+1], "0x%x", &myintnum);

  p = CheckParmWithArgs("-hs",1);
  if (p)
    h_sensitivity = atoi(myargv[p+1]);

  p = CheckParmWithArgs("-vs", 1);
  if (p)
    v_sensitivity = atoi(myargv[p+1]);

  p = CheckParmWithArgs("-mb1",1);
  if (p) sscanf(myargv[p+1], "0x%x", &clickactions[0]);
  p = CheckParmWithArgs("-mb2",1);
  if (p) sscanf(myargv[p+1], "0x%x", &clickactions[1]);
  p = CheckParmWithArgs("-mb3",1);
  if (p) sscanf(myargv[p+1], "0x%x", &clickactions[2]);

  p = CheckParmWithArgs("-db1",1);
  if (p) sscanf(myargv[p+1], "0x%x", &dclickactions[0]);
  p = CheckParmWithArgs("-db2",1);
  if (p) sscanf(myargv[p+1], "0x%x", &dclickactions[1]);
  p = CheckParmWithArgs("-db3",1);
  if (p) sscanf(myargv[p+1], "0x%x", &dclickactions[2]);

  memset (&extdata, 0, sizeof(externdata_t));
  extdata.vector = myintnum;

  // add extra args for passing the address of the buffer to the game
  // and also to disable the game's internal mouse support
  spawnargs = malloc ((myargc+4)*sizeof(char*));
  memcpy (spawnargs, myargv, myargc*sizeof(char*));
  spawnargs[myargc] = "-externdriver";
  spawnargs[myargc+1] = alloca(32);
  flataddr = (long)FP_SEG(&extdata)*16 + (unsigned)(&extdata);
  sprintf (spawnargs[myargc+1], "%li", flataddr);
  spawnargs[myargc+2] = "-nomouse";
  spawnargs[myargc+3] = NULL;

  // custom game launch command specified?
  p = CheckParmWithArgs("-exec", 1);
  if (!p) p = CheckParmWithArgs("-game", 1);
  if (!p) p = CheckParmWithArgs("-exe", 1);
  
  if (p)
    launchcmd = myargv[p+1];
  // else try the defaults
  else
  {
    if (!access("HERETIC.EXE", 0) || !access("HERETIC.COM", 0))
      launchcmd = "heretic";
    else if (!access("HEXEN.EXE", 0) || !access("HEXEN.COM", 0))
      launchcmd = "hexen";
    else
    {
      Error ("No Heretic or Hexen executables in current directory and no\n"\
             "alternate launch command was specified.");
    }
  }

  // hook the interrupt
  vector = *(char far * far *)(myintnum*4);
  if (vector != NULL && *vector != 0xCF) // 0xCF = x86 IRET
    Error ("Interrupt vector 0x%02X is already hooked.", myintnum);

  saved_vector = _dos_getvect (myintnum);
  _dos_setvect (myintnum, (void interrupt (*)(void))MK_FP(FP_SEG(ControlISR),(int)ControlISR));
  vector_is_hooked = 1;

  // launch the game
  if (spawnv (P_WAIT, launchcmd, (const char **)spawnargs) == -1)
    printf ("spawnv(%s) failed: %i (%s)\n", launchcmd, errno, strerror(errno));

  // restore interrupt
  _dos_setvect (myintnum, saved_vector);

  return 0;
}

 


It supports binding actions to double clicks now. The default double click timing window is 20 gametics (approx. 0,6 seconds), the same length as the internal mouse support of those games use, this can be adjusted from the command line with -dctics. By default, double clicking the left mouse button does nothing, double clicking the right button acts as the use key and double clicking the middle button will center your view.

View centering also actually works now (from the mouse, still not from keyboard but in the previous version it didn't work at all) and is why I added double clicking support, since it's not an action I wanted to waste a button on but which can still be useful: level changes, respawns and restarting the game or loading a save from the menu can cause the game's idea and the driver's idea of the current view pitch to go out of sync with each other, which will prevent you from looking up/down all the way until you resynchronize them. In the previous version this could be done by moving the mouse enough in the direction opposite the way you couldn't look, but now you can just bind something to the centerview action.

Also, it automatically passes -nomouse to the game now so you don't have to write it yourself anymore or remember to disable internal mouse support from setup.

I wish there was a way to get notified of level changes, restarts and any other events which may reset the view pitch but seems that's not possible without patching the games (and might as well make the mlook support internal at that point...)

Share this post


Link to post

Dos your Heretic patch build onto Heretic Plus? (You know, Heretic, patched to support higher limits, like visplanes, and samegame size?). By the way, very nice modifications - this is great stuff!

Share this post


Link to post
kb1 said:

Dos your Heretic patch build onto Heretic Plus? (You know, Heretic, patched to support higher limits, like visplanes, and samegame size?). By the way, very nice modifications - this is great stuff!


It does not need to patch the EXE at all so yes, it'll work with Heretic+ or Hexen+. It would work even if you recompiled Heretic or Hexen from sources using Open Watcom with some drastic changes as long as you keep the -externdriver API intact.

Share this post


Link to post
xttl said:

It does not need to patch the EXE at all so yes, it'll work with Heretic+ or Hexen+. It would work even if you recompiled Heretic or Hexen from sources using Open Watcom with some drastic changes as long as you keep the -externdriver API intact.

Oh, duh, I got you. Very cool!

Share this post


Link to post

Guys, Please re-upload the ravmouse.exe binary. I really need it for real hardware.

 

If no one has it, I'll try to compile from source, but I have NO IDEA how it's done.

Share this post


Link to post

Thanks, chungy! 👍

I'll have it running this week. My collection of ancient WASD-moselook games is shaping up! (Future Shock, Duke3D, Eradicator and now Heretic)

 

Is there something similar for Doom?

As of now - I have to resort to modifying MOUSE.COM parameters to kill mouse Y-axis and make x-axis more sensitive. It works, but not on any PC.

Share this post


Link to post
32 minutes ago, Volo said:

Is there something similar for Doom?

 

You could use an old DOS port like Doom Legacy, but no, there is no freelook in standard Doom.

Share this post


Link to post
43 minutes ago, Volo said:

Is there something similar for Doom?

Contrarily to Heretic, Hexen, and Strife; Doom (as well as Chex Quest which only had minimal changes) do not have a concept of looking up and down, so there's no hook in the game code for that. You need to provide a custom engine with this function plugged in, and that's where you're directed to the width and breadth of source port choices.

Share this post


Link to post

Thanks guys,

I don't want mouselook in Doom. I want to stop the doomguy from mouse-running. Just to kill Y-axis altogether. I do it by manipulating drivers (made a .bat file for starting the game), but that's, ahem, not the most elegant solution. Is there any better?

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×