Maker Pro
Arduino This is a fork of Tink Keys

CoH Quick Sell

March 05, 2024 by Chuck Glenn
Share
banner

Before Tink Keys, I was using the KeyPad library on one of these. I had some code to quickly click mouse buttons to sell things to vendors when they are in numbered stacks. This would occasionally jam if I pressed keys while the code was still doing mouse clicks and delays. Here is the Tink Key type of solution: a queue of click sequences!

The behavior

This is clicking the mouse button for you, so VERY IMPORTANT: your mouse must be positioned over the SELL button, and the to-be-sold item in the list must be highlighted.

When clicking the SELL button, you can click down to quantity of 2 really fast, but when it's 2, for whatever reason, you have to pause a bit, then also pause a bit when selling the last 1. This code does all that for you. If you have a quantity of 7, for instance, just hit the 7. It will send 6 quick mouse clicks, then a delay, then the last click, then another delay (in case there is something else in the queue).

Because it sometimes goes wonky anyway, use the * key (KEY_X in the code) to cancel and reset the queue and stop everything.

We handle 10 using the 0 key (because you'd never need to sell zero of something, right?). For 11-20, press the # first (KEY_H in the code), and that will add 10 to whatever you press next. I didn't make the effort to handle stacks of more than 20. You can just hit zero to get rid of ten of them, and there will be 2 unwanted delays in there, but It's so rare I didn't make the effort to handle it.

The game was still losing track until I increased the pause from 3 to 5. Now I'm easily able to enter four quantities at a time and it stays on track. For instance, if I have 7 of the highlighted thing followed by 9 of the next thing down, then 13 then 3, I just hit 79#33, and it gobbles up those four items, then I put in the next four quantities.

Source code doing everything described above

Install the Arduino software and the teensyduino extension, and be sure to select the correct device and port, and to select a USB type that includes Keyboard and Mouse. Happy Tinkering!

/*
tink_keys project 4x4 Quick Sell assistant
*/
#define NO_KEY 0
#define NO_CHANGE 0
#define MODS_BELOW 0xF000
#define PRESSED 1
#define RELEASED 2
#define KEY_LIMIT 6
#define AUTO_TRIGGER 25

/*
DEFINE YOUR KEYS HERE! 
*/
const byte ROWS = 4;
const byte COLS = 4;
uint16_t keys[ROWS][COLS] = {
  { KEY_1, KEY_2, KEY_3, KEY_A },
  { KEY_4, KEY_5, KEY_6, KEY_B },
  { KEY_7, KEY_8, KEY_9, KEY_C },
  { KEY_X, KEY_0, KEY_H, KEY_D }
};
byte rowPins[ROWS] = {4, 3, 2, 1}; 
byte colPins[COLS] = {8, 7, 6, 5};

/*
These control most of what's going on, and shouldn't be used directly in custom code.
*/
bool isDown[ROWS][COLS];
bool wasDown[ROWS][COLS];
uint16_t downCount[ROWS][COLS];
bool isAuto[ROWS][COLS];
bool isCustom[ROWS][COLS];


// these gloss over debounce by scanning only once per 20 millis. These also shouldn't be changed.
uint32_t startTime = millis();
uint32_t debounceTime = 20;  // 1/50 second between scans to reduce debounce

/*
  Simplified vars for custom code use. Almost always only one key at a time is pressed or released.
  Although that's not strictly always true, it's close enough for typical cases.
  actionKey is the key pressed or release, or NO_KEY if nothing changed in the last scan.
  keyAction is NO_CHANGE (and key will be NO_KEY) or PRESSED or RELEASED (with an actual key value).
  actionModifier will be MODIFIER* keys above, logically OR'd together, or in most cases, one of them alone, 
  or ZERO (meaning none of the MODIFIERS were down when the actionKey was PRESSED or RLEASED).
*/
uint16_t actionKey = NO_KEY;
uint16_t actionModifier = 0;
byte keyAction = NO_CHANGE;

/*
  Simple stack to allow custom code to inject keys without messing up timing.
  Don't use these directly! to inject a key press use sendKey(key) from custom code.
*/
#define INJECT_LIMIT 32
uint16_t injectKeys[INJECT_LIMIT];
byte injectIdx = 0;


void setup() {
  setAllDefaults();
  // CUSTOM or AUTO are indicated here:
  // everything in columns 0, 1 and 2 are custom
  for (byte r = 0; r < ROWS; r++) {
    isCustom[r][0] = true;
    isCustom[r][1] = true;
    isCustom[r][2] = true;
  }
}

// CUSTOMIZATION VARS HERE -- anything that needs to persist across multiple loop() iterations.
/* simplified queue: 
write[idx++%]
read[idx++%] 
*/
#define QUEUE_SIZE 32
int queue[QUEUE_SIZE];
byte qReadIdx = 0;
byte qWriteIdx = 0;

/* our clicks (not including delays) will be this many cycles, 
where each cycle is 20 ms, 
so 100ms or 1/10th second click speed
this has to be bigger than 3 because we press mouse button on 0 and release on 2
*/
#define CLICK_CYCLE 5
byte clickCounter = 0;

// just to avoid spamming release of buttons, we set this to true when the button is down
bool mouseDown = false;

// when we need a pause, this will be > 0
#define PAUSE_AMOUNT 5
int pauseCycle = 0;

// in cases of amounts > 10, we click # to add 10 to whatever we send next. > 20 is very rare, so we'll just gloss over that case
bool plusTen = false;

void loop() {
  if ((millis() - startTime) > debounceTime) {
    scanKeys();
    /* CUSTOM CODE GOES HERE */
    // key pad clicks WRITE to the queue (or turn off everything)
    if (keyAction == PRESSED) {
      if (actionKey == KEY_X) { // * for STOP EVERYTHING! SOMETHING WENT FUBAR
        pauseCycle = 0;
        qReadIdx = qWriteIdx;
        queue[qReadIdx] = 0;
      } else if (actionKey == KEY_H) {
        plusTen = true;
      } else if (actionKey >= KEY_1 and actionKey <= KEY_0) { // behind-the-scenes numbers for these run from 1-9 then 0, so you can treat 0 like 10
        // write to the queue plus ten if appropriate, and reset plusTen
        queue[qWriteIdx] = (actionKey - KEY_1) + 1;
        if (plusTen) {
          queue[qWriteIdx] += 10;
          plusTen = false;
        }
        qWriteIdx = (qWriteIdx + 1) % QUEUE_SIZE; // then bump index
      }
    }
    // READING from the queue: process readidx until 0 and until pauses are 0
    if (clickCounter == 0) {
      if (pauseCycle > 0) {
        pauseCycle--;
      } else if (qReadIdx != qWriteIdx) {
        Mouse.set_buttons(1);
        mouseDown = true;
        if (queue[qReadIdx] < 3) {
          pauseCycle = PAUSE_AMOUNT;
        }
        queue[qReadIdx]--;
        if (queue[qReadIdx] == 0) {
          qReadIdx = (qReadIdx + 1) % QUEUE_SIZE;
        }
      }
    } else if (clickCounter == 2 && mouseDown) {
      Mouse.set_buttons(0);
      mouseDown = false;
    }
    clickCounter = (clickCounter + 1) % CLICK_CYCLE;

    // END OF CUSTOM CODE -- leave below stuff alone!
    complexOutput(); // handles all normal and auto keys, and includes anything you added via sendKey() above
    startTime = millis();
  }
}

// EVERYTHING BELOW DOES NOT CONTAIN CUSTOM CODE AND SHOULD BE LEFT ALONE

// void setAsCustom(uint16_t key) {
//   for (byte c = 0; c < COLS; c++) {
//     for (byte r = 0; r < ROWS; r++) { 
//       if (keys[r][c] == key) {
//         isCustom[r][c] = true;
//         return;
//       }
//     }
//   }
// }

// void setAsAuto(uint16_t key) {
//   for (byte c = 0; c < COLS; c++) {
//     for (byte r = 0; r < ROWS; r++) { 
//       if (keys[r][c] == key) {
//         isAuto[r][c] = true;
//         return;
//       }
//     }
//   }
// }

// setup default values into all the under-the-hood arrays
void setAllDefaults() {
   // initialize everything to defaults
  for (byte c = 0; c < COLS; c++) {
    for (byte r = 0; r < ROWS; r++) {
      isDown[r][c] = false;
      wasDown[r][c] = false;
      downCount[r][c] = 0;
      isAuto[r][c] = false;
      isCustom[r][c] = false;
    }
  }
}

// for simplicity, even though this is slower than specifying [row][col] directly, it's better for the custom code if you move or reassign keys and such

bool isKeyDown(uint16_t key) {
  for (byte c = 0; c < COLS; c++) {
    for (byte r = 0; r < ROWS; r++) { 
      if (keys[r][c] == key) {
        return isDown[r][c];
      }
    }
  }
  return false;
}

int heldHowLong(uint16_t key) {
  for (byte c = 0; c < COLS; c++) {
    for (byte r = 0; r < ROWS; r++) { 
      if (keys[r][c] == key) {
        return downCount[r][c] * debounceTime; // approximate only! but close enough
      }
    }
  }
  return 0;
}

// handles non-custom and auto keys
void complexOutput() {
  actionModifier = 0;
  uint16_t downKeys[KEY_LIMIT] = {0, 0, 0, 0, 0, 0};
  byte idx = 0;
  for (byte c = 0; c < COLS && idx < KEY_LIMIT; c++) {
    for (byte r = 0; r < ROWS && idx < KEY_LIMIT; r++) { 
      if ( ! isCustom[r][c]) {
        if (isDown[r][c] || (isAuto[r][c] && downCount[r][c] > AUTO_TRIGGER) ) {
          if (keys[r][c] < MODS_BELOW) {
            actionModifier = actionModifier | keys[r][c];
          } else {
            downKeys[idx] = keys[r][c];
            idx++;
          }
        }
      }
    }
  }
  // add in any injected keys from custom code (ONLY IF MODIFIERS ARE ZERO!, we don't want to send alt-1 when we were supposed to send 1 for example)
  while (idx < KEY_LIMIT && injectIdx > 0 && actionModifier == 0) {
    injectIdx--;
    uint16_t addKey = injectKeys[injectIdx];
    downKeys[idx] = addKey;
    idx++;
  }
  // send these updates to the PC (ONLY ON CHANGES)
  sendIfChanged(actionModifier, downKeys[0], downKeys[1], downKeys[2], downKeys[3], downKeys[4], downKeys[5]);
}


uint16_t lastModifier = 0;
uint16_t lastKey1 = 0;
uint16_t lastKey2 = 0;
uint16_t lastKey3 = 0;
uint16_t lastKey4 = 0;
uint16_t lastKey5 = 0;
uint16_t lastKey6 = 0;
void sendIfChanged(uint16_t modifier, uint16_t key1, uint16_t key2, uint16_t key3, uint16_t key4, uint16_t key5, uint16_t key6) {
   if (modifier != lastModifier || key1 != lastKey1 || key2 != lastKey2 || key3 != lastKey3 || key4 != lastKey4 || key5 != lastKey5 || key6 != lastKey6) {
    Keyboard.set_modifier(modifier);
    Keyboard.set_key1(key1);
    Keyboard.set_key2(key2);
    Keyboard.set_key3(key3);
    Keyboard.set_key4(key4);
    Keyboard.set_key5(key5);
    Keyboard.set_key6(key6);
    Keyboard.send_now();
    lastModifier = modifier;
    lastKey1 = key1;
    lastKey2 = key2;
    lastKey3 = key3;
    lastKey4 = key4;
    lastKey5 = key5;
    lastKey6 = key6;
  }
}

// stupidly simple stack via array, ignored if limits exceeded
void sendKey(uint16_t key) {
  if (injectIdx < INJECT_LIMIT) {
    injectKeys[injectIdx] = key;
    injectIdx++;
  }
}

void scanKeys() {
  // copy current status to old status
  for (byte c = 0; c < COLS; c++) {
    for (byte r = 0; r < ROWS; r++) {
      wasDown[r][c] = isDown[r][c];
    }
  }

  // scan using COLUMNS defaulting to HIGH, and setting them to LOW in a loop...
  // then looking at ROWS for LOWS (meaning it's curently down)
  for (byte r = 0; r < ROWS; r++) {
    pinMode(rowPins[r], INPUT_PULLUP);
  }
  for (byte c = 0; c < COLS; c++) {
    pinMode(colPins[c], OUTPUT);
    digitalWrite(colPins[c], LOW);
    for (byte r = 0; r < ROWS; r++) {
      isDown[r][c] = (LOW == digitalRead(rowPins[r]));
    }
    digitalWrite(colPins[c], HIGH);
    pinMode(colPins[c], INPUT);
  }
  
  // simplified most-recent key and action (for toggle code in the loop() method)
  keyAction = NO_CHANGE;
  actionKey = NO_KEY;

  for (byte c = 0; c < COLS && keyAction == NO_CHANGE; c++) {
    for (byte r = 0; r < ROWS && keyAction == NO_CHANGE; r++) {
      if (wasDown[r][c] != isDown[r][c]) {
        if (isDown[r][c]) {
          keyAction = PRESSED;
          if (keys[r][c] != NO_KEY) { // we only set this when a key is PRESSED
            actionKey = keys[r][c];
          }
          downCount[r][c] = 0; // on PRESS we reset the held down counter
        } else {
          keyAction = RELEASED;
        }
      }
      if (isDown[r][c]) {
        downCount[r][c]++;
      }
    }
  }
}

Related Content

Comments


You May Also Like