Skip to content

Conversation

Rampoina
Copy link
Contributor

@Rampoina Rampoina commented Jul 18, 2022

This is a proposal to add hotkeys for every command.
This adds 10 configurable keys that map to the commands as displayed in the GUI.
It's meant to be used in a grid layout such as QWER ASDF ZXCV to make the locations more intuitive. (although it doesn't have to, users can set each hotkey wherever they want to)

Fixes #212

@andy5995
Copy link
Collaborator

@titiger would you like to test this with me and @Rampoina soon? Is this something you'd consider merging?

@Jammyjamjamman Jammyjamjamman self-requested a review July 19, 2022 21:55
@andy5995
Copy link
Collaborator

@Jammyjamjamman The controls are listed here in the README: https://raw.githubusercontent.com/MegaGlest/megaglest-source/master/docs/README.txt Is that generated from a script?

Copy link
Contributor

@Jammyjamjamman Jammyjamjamman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good so far 👍 . @andy5995 and I gave it a test. I've listed changes that I think are required, based on our testing.

Copy link
Contributor Author

@Rampoina Rampoina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commit adds the hotkey text on hover. It's going to need translations for the text "Hotkey".

@Jammyjamjamman Jammyjamjamman self-requested a review July 28, 2022 17:37
Copy link
Contributor

@Jammyjamjamman Jammyjamjamman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. I don't have the final say on merging this but the changes look good to me.

@pavanvo
Copy link
Contributor

pavanvo commented Aug 2, 2022

Yo @Rampoina , Good work!!! but wait up, tech engineer can't build Balista with hotkey. We need add at least 11 command.
And Summoner can't update to dragon, cuz we checking by numberCommands
at source/glest_game/gui/gui.cpp
line 460
if (i < numberCommands) {
mouseDownDisplayUnitSkills(i);
computeDisplay();
}
but some units like above have 3 commands in second line!

@Rampoina
Copy link
Contributor Author

Rampoina commented Aug 2, 2022

@pavanvo thanks for testing!
I've bumped the number of hotkeys to 12 (the full grid) and fixed the issue with the morphing units (numberCommands wasn't updated correctly again ... I don't know if I'm missing something here, it seems like a variable that should exist, but I wasn't able to find it)

@pavanvo
Copy link
Contributor

pavanvo commented Aug 2, 2022

@Rampoina you forget to commit gui.h
Now, with simple increment, it's working fine, but I'm trying to create another kind of check.

@Rampoina
Copy link
Contributor Author

Rampoina commented Aug 2, 2022

@pavanvo It's not only an increment, now it uses posDisplay which is updated inside the previous for, including the case for morphing units.
(I force pushed the missing gui.h in the same commit)

@andy5995
Copy link
Collaborator

andy5995 commented Aug 8, 2022

@Jammyjamjamman The controls are listed here in the README: https://raw.githubusercontent.com/MegaGlest/megaglest-source/master/docs/README.txt Is that generated from a script?

Seems like this hasn't been done yet. @Jammyjamjamman imo this should be done before this gets merged.

Copy link
Collaborator

@andy5995 andy5995 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently there are conflicts (as shown at the bottom).

@Rampoina Rampoina force-pushed the hotkeys branch 3 times, most recently from 449b944 to a5438f6 Compare August 14, 2022 11:42
@Jammyjamjamman
Copy link
Contributor

There's some mods that crash the game with this patch:

There might be more that don't work. I'm not sure what causes the crash. In e.g. prax the crash happens when loading "hexer".

@pavanvo
Copy link
Contributor

pavanvo commented Aug 28, 2022

There's some mods that crash the game with this patch:

* prax 0.5.9.7 (find in mod center)

* https://github.com/Robotkiller001/Megaglest-Improved-Mod

* https://github.com/Robotkiller001/Insects-World-Mod

There might be more that don't work. I'm not sure what causes the crash. In e.g. prax the crash happens when loading "hexer".

i am tested on linux and windows - everything works fine, I think problem in build
I found problem with "haxer"

@andy5995
Copy link
Collaborator

andy5995 commented Feb 26, 2025

I added a patch for these changes in #317 (There are some merge conflicts in this PR, so I fixed them locally first and then generated a patch using git diff)

@pavanvo
Copy link
Contributor

pavanvo commented Feb 26, 2025

@andy5995
Copy link
Collaborator

@Rampoina @pavanvo I reverted the commits that had already been merged into develop, which brought the "files changed" down from 31 to 10. I accidentally added the data submodule but that's a minor point. I formatted the files so rebasing, if anyone ever does it, shouldn't be too difficult. I tried doing it but it was too convoluted due to the way the commits were done on this PR, so I'm sure I would have got something wrong if I had continued.

@andy5995 andy5995 force-pushed the hotkeys branch 3 times, most recently from 73dcb54 to 88641f2 Compare March 26, 2025 19:15
@andy5995
Copy link
Collaborator

Any merge conflicts remaining now are not due to the code reformatting that's been merged into develop recently.

But quite unexplainedly, the diff for this PR shown on the Files tab is different than actual. For example, when I compare gui.cpp locally, I get the expected result (some differences are due to the unit portait PR that was merged last month):

$ git diff origin/develop hotkeys ../../source/glest_game/gui/gui.cpp
diff --git a/source/glest_game/gui/gui.cpp b/source/glest_game/gui/gui.cpp
index 2a8464199..56c3bea60 100644
--- a/source/glest_game/gui/gui.cpp
+++ b/source/glest_game/gui/gui.cpp
@@ -38,7 +38,7 @@ namespace Glest {
 namespace Game {
 
 // =====================================================
-//     class Mouse3d
+//     class Mouse3d
 // =====================================================
 
 const float Mouse3d::fadeSpeed = 1.f / 50.f;
@@ -65,7 +65,7 @@ void Mouse3d::update() {
 }
 
 // ===============================
-//     class SelectionQuad
+//     class SelectionQuad
 // ===============================
 
 SelectionQuad::SelectionQuad() {
@@ -85,7 +85,7 @@ void SelectionQuad::setPosUp(const Vec2i &posUp) { this->posUp = posUp; }
 void SelectionQuad::disable() { enabled = false; }
 
 // =====================================================
-//     class Gui
+//     class Gui
 // =====================================================
 
 // constructor
@@ -100,7 +100,6 @@ Gui::Gui() {
   activeCommandType = NULL;
   activeCommandClass = ccStop;
   selectingBuilding = false;
-  hoveringUnitPortrait = false;
   selectedBuildingFacing = CardinalDir(CardinalDir::NORTH);
   selectingPos = false;
   selectingMeetingPoint = false;
@@ -203,7 +202,6 @@ void Gui::invalidatePosObjWorld() { validPosObjWorld = false; }
 
 void Gui::resetState() {
   selectingBuilding = false;
-  hoveringUnitPortrait = false;
   selectedBuildingFacing = CardinalDir(CardinalDir::NORTH);
   selectingPos = false;
   selectingMeetingPoint = false;
@@ -234,18 +232,14 @@ void Gui::mouseDownLeftDisplay(int x, int y) {
     int posDisplay = computePosDisplay(x, y);
 
     if (posDisplay != invalidPos) {
-      if (hoveringUnitPortrait) {
-        mouseDownPortrait(posDisplay);
-      } else {
-        if (selection.isCommandable()) {
-          if (selectingBuilding) {
-            mouseDownDisplayUnitBuild(posDisplay);
-          } else {
-            mouseDownDisplayUnitSkills(posDisplay);
-          }
+      if (selection.isCommandable()) {
+        if (selectingBuilding) {
+          mouseDownDisplayUnitBuild(posDisplay);
         } else {
-          resetState();
+          mouseDownDisplayUnitSkills(posDisplay);
         }
+      } else {
+        resetState();
       }
     }
     computeDisplay();
@@ -412,18 +406,17 @@ void Gui::hotKey(SDL_KeyboardEvent key) {
     centerCameraOnSelection();
   }
   // else if(key == configKeys.getCharKey("HotKeySelectIdleHarvesterUnit")) {
-  else if (isKeyPressed(configKeys.getSDLKey("HotKeySelectIdleHarvesterUnit"),
-                        key) == true) {
+  if (isKeyPressed(configKeys.getSDLKey("HotKeySelectIdleHarvesterUnit"),
+                   key) == true) {
     selectInterestingUnit(iutIdleHarvester);
   }
   // else if(key == configKeys.getCharKey("HotKeySelectBuiltBuilding")) {
-  else if (isKeyPressed(configKeys.getSDLKey("HotKeySelectBuiltBuilding"),
-                        key) == true) {
+  if (isKeyPressed(configKeys.getSDLKey("HotKeySelectBuiltBuilding"), key) ==
+      true) {
     selectInterestingUnit(iutBuiltBuilding);
   }
   // else if(key == configKeys.getCharKey("HotKeyDumpWorldToLog")) {
-  else if (isKeyPressed(configKeys.getSDLKey("HotKeyDumpWorldToLog"), key) ==
-           true) {
+  if (isKeyPressed(configKeys.getSDLKey("HotKeyDumpWorldToLog"), key) == true) {
     if (SystemFlags::getSystemSettingType(SystemFlags::debugSystem).enabled ==
         true) {
       std::string worldLog = world->DumpWorldToLog();
@@ -434,8 +427,8 @@ void Gui::hotKey(SDL_KeyboardEvent key) {
     }
   }
   // else if(key == configKeys.getCharKey("HotKeyRotateUnitDuringPlacement")){
-  else if (isKeyPressed(configKeys.getSDLKey("HotKeyRotateUnitDuringPlacement"),
-                        key) == true) {
+  if (isKeyPressed(configKeys.getSDLKey("HotKeyRotateUnitDuringPlacement"),
+                   key) == true) {
     // Here the user triggers a unit rotation while placing a unit
     if (isPlacingBuilding()) {
       if (getBuilding()->getRotationAllowed()) {
@@ -444,25 +437,39 @@ void Gui::hotKey(SDL_KeyboardEvent key) {
     }
   }
   // else if(key == configKeys.getCharKey("HotKeySelectDamagedUnit")) {
-  else if (isKeyPressed(configKeys.getSDLKey("HotKeySelectDamagedUnit"), key) ==
-           true) {
+  if (isKeyPressed(configKeys.getSDLKey("HotKeySelectDamagedUnit"), key) ==
+      true) {
     selectInterestingUnit(iutDamaged);
   }
   // else if(key == configKeys.getCharKey("HotKeySelectStoreUnit")) {
-  else if (isKeyPressed(configKeys.getSDLKey("HotKeySelectStoreUnit"), key) ==
-           true) {
+  if (isKeyPressed(configKeys.getSDLKey("HotKeySelectStoreUnit"), key) ==
+      true) {
     selectInterestingUnit(iutStore);
   }
   // else if(key == configKeys.getCharKey("HotKeySelectedUnitsAttack")) {
-  else if (isKeyPressed(configKeys.getSDLKey("HotKeySelectedUnitsAttack"),
-                        key) == true) {
+  if (isKeyPressed(configKeys.getSDLKey("HotKeySelectedUnitsAttack"), key) ==
+      true) {
     clickCommonCommand(ccAttack);
   }
   // else if(key == configKeys.getCharKey("HotKeySelectedUnitsStop")) {
-  else if (isKeyPressed(configKeys.getSDLKey("HotKeySelectedUnitsStop"), key) ==
-           true) {
+  if (isKeyPressed(configKeys.getSDLKey("HotKeySelectedUnitsStop"), key) ==
+      true) {
     clickCommonCommand(ccStop);
   }
+
+  for (int i = 0; i < commandKeys; i++) {
+    string name = "CommandKey" + intToStr(i + 1);
+    if (isKeyPressed(configKeys.getSDLKey(name.c_str()), key) == true) {
+      if (activeCommandType != NULL &&
+          activeCommandType->getClass() == ccBuild) {
+        mouseDownDisplayUnitBuild(i);
+      } else {
+        mouseDownDisplayUnitSkills(i);
+      }
+      computeDisplay();
+      break;
+    }
+  }
 }
 
 void Gui::switchToNextDisplayColor() { display.switchColor(); }
@@ -489,6 +496,8 @@ void Gui::giveOneClickOrders() {
   addOrdersResultToConsole(activeCommandClass, result);
   activeCommandType = NULL;
   activeCommandClass = ccStop;
+  selectingPos = false;
+  activePos = invalidPos;
 }
 
 void Gui::giveDefaultOrders(int x, int y) {
@@ -662,17 +671,6 @@ void Gui::clickCommonCommand(CommandClass commandClass) {
   computeDisplay();
 }
 
-void Gui::mouseDownPortrait(int posDisplay) {
-  Unit *unit = selection.getUnitPtr(posDisplay);
-  if (isKeyDown(vkControl)) {
-    selection.selectType(unit);
-  } else if (!isKeyDown(vkShift)) {
-    selection.clear();
-    selection.select(unit, false);
-  } else {
-    selection.unSelect(posDisplay);
-  }
-}
 void Gui::mouseDownDisplayUnitSkills(int posDisplay) {
   if (selection.isEmpty() == false) {
     if (posDisplay != cancelPos) {
@@ -683,28 +681,6 @@ void Gui::mouseDownDisplayUnitSkills(int posDisplay) {
         if (selection.isUniform()) {
           const CommandType *ct = display.getCommandType(posDisplay);
 
-          // try to switch to next attack type
-          if (activeCommandClass == ccAttack && activeCommandType != NULL) {
-            int maxI = unit->getType()->getCommandTypeCount();
-            int cmdTypeId = activeCommandType->getId();
-            int cmdTypeIdNext = cmdTypeId + 1;
-
-            while (cmdTypeIdNext != cmdTypeId) {
-              if (cmdTypeIdNext >= maxI) {
-                cmdTypeIdNext = 0;
-              }
-              const CommandType *ctype = display.getCommandType(cmdTypeIdNext);
-              if (ctype != NULL && ctype->getClass() == ccAttack) {
-                if (ctype != NULL && unit->getFaction()->reqsOk(ctype)) {
-                  posDisplay = cmdTypeIdNext;
-                  ct = display.getCommandType(posDisplay);
-                  break;
-                }
-              }
-              cmdTypeIdNext++;
-            }
-          }
-
           if (ct != NULL && unit->getFaction()->reqsOk(ct)) {
             activeCommandType = ct;
             activeCommandClass = activeCommandType->getClass();
@@ -714,10 +690,7 @@ void Gui::mouseDownDisplayUnitSkills(int posDisplay) {
             activeCommandClass = ccStop;
             return;
           }
-        }
-
-        // non uniform selection
-        else {
+        } else {  // non uniform selection
           activeCommandType = NULL;
           activeCommandClass = display.getCommandClass(posDisplay);
           if (activeCommandClass == ccAttack) {
@@ -726,7 +699,7 @@ void Gui::mouseDownDisplayUnitSkills(int posDisplay) {
         }
 
         // give orders depending on command type
-        if (!selection.isEmpty()) {
+        if (activeCommandClass != ccNull) {
           const CommandType *ct =
               selection.getUnit(0)->getType()->getFirstCtOfClass(
                   activeCommandClass);
@@ -735,10 +708,13 @@ void Gui::mouseDownDisplayUnitSkills(int posDisplay) {
                 selection.getUnitFromCC(ccAttack)->getType()->getFirstCtOfClass(
                     activeCommandClass);
           }
+
           if (activeCommandType != NULL &&
               activeCommandType->getClass() == ccBuild) {
             assert(selection.isUniform());
             selectingBuilding = true;
+            selectingPos = false;
+            activePos = invalidPos;
           } else if (ct->getClicks() == cOne) {
             invalidatePosObjWorld();
             giveOneClickOrders();
@@ -746,6 +722,8 @@ void Gui::mouseDownDisplayUnitSkills(int posDisplay) {
             selectingPos = true;
             activePos = posDisplay;
           }
+        } else {
+          posDisplay = invalidPos;
         }
       } else {
         activePos = posDisplay;
@@ -766,7 +744,7 @@ void Gui::mouseDownDisplayUnitBuild(int posDisplay) {
     if (activeCommandType != NULL && activeCommandType->getClass() == ccBuild) {
       const BuildCommandType *bct =
           dynamic_cast<const BuildCommandType *>(activeCommandType);
-      if (bct != NULL) {
+      if (bct != NULL && bct->getBuildingCount() > posDisplay) {
         const UnitType *ut = bct->getBuilding(posDisplay);
 
         const Unit *unit = selection.getFrontUnit();
@@ -803,9 +781,17 @@ void Gui::computeInfoString(int posDisplay) {
   lastPosDisplay = posDisplay;
 
   display.setInfoText(computeDefaultInfoString());
-
-  if (!hoveringUnitPortrait && posDisplay != invalidPos &&
-      selection.isCommandable()) {
+  Config &configKeys = Config::getInstance(
+      std::pair<ConfigType, ConfigType>(cfgMainKeys, cfgUserKeys));
+  string commandKeyName = "CommandKey" + intToStr(posDisplay + 1);
+
+  if (posDisplay != invalidPos && selection.isCommandable()) {
+    string hotkey = "";
+    if (posDisplay < commandKeys) {
+      hotkey = lang.getString("HotKey") + ": " +
+               SDL_GetKeyName(configKeys.getSDLKey(commandKeyName.c_str())) +
+               "\n\n";
+    }
     if (!selectingBuilding) {
       if (posDisplay == cancelPos) {
         display.setInfoText(lang.getString("Cancel"));
@@ -819,11 +805,12 @@ void Gui::computeInfoString(int posDisplay) {
 
           if (ct != NULL) {
             if (unit->getFaction()->reqsOk(ct)) {
-              display.setInfoText(ct->getDesc(unit->getTotalUpgrade(),
+              display.setInfoText(hotkey +
+                                  ct->getDesc(unit->getTotalUpgrade(),
                                               game->showTranslatedTechTree()));
             } else {
               display.setInfoText(
-                  ct->getReqDesc(game->showTranslatedTechTree()));
+                  hotkey + ct->getReqDesc(game->showTranslatedTechTree()));
               if (ct->getClass() == ccUpgrade) {
                 string text = "";
                 const UpgradeCommandType *uct =
@@ -836,7 +823,8 @@ void Gui::computeInfoString(int posDisplay) {
                   text = lang.getString("AlreadyUpgraded") + "\n\n";
                 }
                 display.setInfoText(
-                    text + ct->getReqDesc(game->showTranslatedTechTree()));
+                    hotkey + text +
+                    ct->getReqDesc(game->showTranslatedTechTree()));
               }
               // locked by scenario
               else if (ct->getClass() == ccProduce) {
@@ -845,7 +833,7 @@ void Gui::computeInfoString(int posDisplay) {
                     static_cast<const ProduceCommandType *>(ct);
                 if (unit->getFaction()->isUnitLocked(pct->getProducedUnit())) {
                   display.setInfoText(
-                      lang.getString("LockedByScenario") + "\n\n" +
+                      hotkey + lang.getString("LockedByScenario") + "\n\n" +
                       ct->getReqDesc(game->showTranslatedTechTree()));
                 }
               } else if (ct->getClass() == ccMorph) {
@@ -853,7 +841,7 @@ void Gui::computeInfoString(int posDisplay) {
                     static_cast<const MorphCommandType *>(ct);
                 if (unit->getFaction()->isUnitLocked(mct->getMorphUnit())) {
                   display.setInfoText(
-                      lang.getString("LockedByScenario") + "\n\n" +
+                      hotkey + lang.getString("LockedByScenario") + "\n\n" +
                       ct->getReqDesc(game->showTranslatedTechTree()));
                 }
               }
@@ -891,7 +879,7 @@ void Gui::computeInfoString(int posDisplay) {
           const Unit *unit = selection.getFrontUnit();
           if (unit->getFaction()->isUnitLocked(bct->getBuilding(posDisplay))) {
             display.setInfoText(
-                lang.getString("LockedByScenario") + "\n\n" +
+                hotkey + lang.getString("LockedByScenario") + "\n\n" +
                 bct->getBuilding(posDisplay)
                     ->getReqDesc(game->showTranslatedTechTree()));
           } else {
@@ -917,7 +905,7 @@ void Gui::computeInfoString(int posDisplay) {
                    ": " + intToStr(seconds);
             str += "\n\n";
             str += building->getReqDesc(translatedValue);
-            display.setInfoText(str);
+            display.setInfoText(hotkey + str);
           }
         }
       }
@@ -990,6 +978,8 @@ void Gui::computeDisplay() {
       // printf("selection.isComandable()\n");
 
       if (selectingBuilding == false) {
+        vector<int> emptyPosIndexes = {};
+
         // cancel button
         const Unit *u = selection.getFrontUnit();
         const UnitType *ut = u->getType();
@@ -1022,10 +1012,14 @@ void Gui::computeDisplay() {
           if (u->isBuilt()) {
             // printf("u->isBuilt()\n");
 
-            int morphPos = 8;
-            for (int i = 0; i < ut->getCommandTypeCount(); ++i) {
+            int morphPos = CommandHelper::getRowPos(crMorphs);
+            for (int i = 0; i < ut->getCommandTypeSortedCount(); ++i) {
               int displayPos = i;
-              const CommandType *ct = ut->getCommandType(i);
+              const CommandType *ct = ut->getCommandTypeSorted(i);
+              if (ct == NULL) {
+                emptyPosIndexes.push_back(displayPos);
+                continue;
+              }
               if (ct->getClass() == ccMorph) {
                 displayPos = morphPos++;
               }
@@ -1083,35 +1077,48 @@ void Gui::computeDisplay() {
         } else {
           // printf("selection.isUniform() == FALSE\n");
           // non uniform selection
-          int lastCommand = 0;
-          for (int i = 0; i < ccCount; ++i) {
-            CommandClass cc = static_cast<CommandClass>(i);
+          int basicPos = CommandHelper::getRowPos(crBasics);
+
+          // Cores row is always empty
+          for (int i = CommandHelper::getRowPos(crCores); i < basicPos; i++)
+            emptyPosIndexes.push_back(i);
 
-            // printf("computeDisplay i = %d cc = %d isshared = %d lastCommand =
-            // %d\n",i,cc,isSharedCommandClass(cc),lastCommand);
+          // only basics can be shared
+          for (auto &&cc : CommandHelper::getBasicsCC()) {
+            // printf("computeDisplay i = %d cc = %d isshared = %d basicPos =
+            // %d\n",i,cc,isSharedCommandClass(cc),basicPos);
 
             const Unit *attackingUnit = NULL;
             if (cc == ccAttack) {
               attackingUnit = selection.getUnitFromCC(ccAttack);
             }
 
+            auto ccPos = CommandHelper::getBasicPos(cc);
+
             if ((cc == ccAttack && attackingUnit != NULL) ||
-                (isSharedCommandClass(cc) && cc != ccBuild)) {
-              display.setDownLighted(lastCommand, true);
+                isSharedCommandClass(cc)) {
+              display.setDownLighted(basicPos + ccPos, true);
 
               if (cc == ccAttack && attackingUnit != NULL) {
-                display.setDownImage(lastCommand, attackingUnit->getType()
-                                                      ->getFirstCtOfClass(cc)
-                                                      ->getImage());
+                display.setDownImage(basicPos + ccPos,
+                                     attackingUnit->getType()
+                                         ->getFirstCtOfClass(cc)
+                                         ->getImage());
               } else {
-                display.setDownImage(lastCommand,
+                display.setDownImage(basicPos + ccPos,
                                      ut->getFirstCtOfClass(cc)->getImage());
               }
-              display.setCommandClass(lastCommand, cc);
-              lastCommand++;
+              display.setCommandClass(basicPos + ccPos, cc);
+            } else {
+              emptyPosIndexes.push_back(basicPos + ccPos);
             }
           }
         }
+        for (int i : emptyPosIndexes) {
+          display.setDownImage(i, ut->getCancelImage());
+          display.setCommandType(i, NULL);
+          display.setDownLighted(i, false);
+        }
       } else if (activeCommandType != NULL &&
                  activeCommandType->getClass() == ccBuild) {
         const Unit *u = selection.getFrontUnit();
@@ -1203,13 +1210,6 @@ int Gui::computePosDisplay(int x, int y) {
   }
 
   // printf("computePosDisplay returning = %d\n",posDisplay);
-  hoveringUnitPortrait = false;
-  if (posDisplay == invalidPos) {
-    posDisplay = display.computeUpIndex(x, y);
-    if (posDisplay != invalidPos) {
-      hoveringUnitPortrait = true;
-    }
-  }
 
   return posDisplay;
 }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add hotkeys for units, buildings and skills
4 participants