Add Drawing mode, which allows drawing some graphics private to the members of a clan draft
authorS.D.
Sun, 16 Oct 2022 13:14:16 +0300
changeset 15908 014f4edd0421
parent 15907 a323e1954a6f
child 15909 7409084d891f
Add Drawing mode, which allows drawing some graphics private to the members of a clan
QTfrontend/binds.cpp
QTfrontend/binds.h
QTfrontend/game.cpp
QTfrontend/game.h
QTfrontend/hwform.cpp
QTfrontend/net/newnetclient.cpp
QTfrontend/net/newnetclient.h
doc/protocol.txt
gameServer/HWProtoInRoomState.hs
hedgewars/CMakeLists.txt
hedgewars/hwengine.pas
hedgewars/uAIActions.pas
hedgewars/uCommandHandlers.pas
hedgewars/uCursor.pas
hedgewars/uDrawing.pas
hedgewars/uIO.pas
hedgewars/uInputHandler.pas
hedgewars/uTouch.pas
hedgewars/uTypes.pas
hedgewars/uVisualGears.pas
hedgewars/uVisualGearsHandlers.pas
hedgewars/uVisualGearsList.pas
hedgewars/uWorld.pas
--- a/QTfrontend/binds.cpp	Thu Oct 06 20:58:54 2022 +0300
+++ b/QTfrontend/binds.cpp	Sun Oct 16 13:14:16 2022 +0300
@@ -85,6 +85,7 @@
     {"!MULTI",    QT_TRANSLATE_NOOP("binds (combination)", "switch + toggle hedgehog tags"), QT_TRANSLATE_NOOP("binds", "toggle hedgehog tag translucency"), NULL, NULL},
 
     {"!MULTI",    QT_TRANSLATE_NOOP("binds (combination)", "precise + switch + toggle team bars"), QT_TRANSLATE_NOOP("binds", "toggle HUD"), NULL, NULL},
+    {"+teamdraw", "d",          QT_TRANSLATE_NOOP("binds", "enter drawing mode"), NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Drawing mode")},
 #ifdef VIDEOREC
     {"record",    "r",          QT_TRANSLATE_NOOP("binds", "record"),          NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Record video:")}
 #endif
--- a/QTfrontend/binds.h	Thu Oct 06 20:58:54 2022 +0300
+++ b/QTfrontend/binds.h	Sun Oct 16 13:14:16 2022 +0300
@@ -22,9 +22,9 @@
 #include <QString>
 
 #ifdef VIDEOREC
-#define BINDS_NUMBER 63
+#define BINDS_NUMBER 64
 #else
-#define BINDS_NUMBER 62
+#define BINDS_NUMBER 63
 #endif
 
 struct BindAction
--- a/QTfrontend/game.cpp	Thu Oct 06 20:58:54 2022 +0300
+++ b/QTfrontend/game.cpp	Sun Oct 16 13:14:16 2022 +0300
@@ -486,6 +486,12 @@
             emit SendTeamMessage(msgbody);
             break;
         }
+        case 'O':
+        {
+            QByteArray msgbody = msg.mid(2);
+            emit SendDrawCmd(msgbody);
+            break;
+        }
         case 'V':
         {
             if (msg.at(2) == '?')
--- a/QTfrontend/game.h	Thu Oct 06 20:58:54 2022 +0300
+++ b/QTfrontend/game.h	Sun Oct 16 13:14:16 2022 +0300
@@ -95,6 +95,7 @@
         void SendNet(const QByteArray & msg);
         void SendChat(const QString & msg);
         void SendTeamMessage(const QString & msg);
+        void SendDrawCmd(const QByteArray & msg);
         void GameStateChanged(GameState gameState);
         void DemoPresenceChanged(bool hasDemo);
         void GameStats(char type, const QString & info);
--- a/QTfrontend/hwform.cpp	Thu Oct 06 20:58:54 2022 +0300
+++ b/QTfrontend/hwform.cpp	Sun Oct 16 13:14:16 2022 +0300
@@ -1943,6 +1943,7 @@
     connect(game, SIGNAL(SendChat(const QString &)), hwnet, SLOT(chatLineToNet(const QString &)));
     connect(game, SIGNAL(SendConsoleCommand(const QString&)), hwnet, SLOT(consoleCommand(const QString&)));
     connect(game, SIGNAL(SendTeamMessage(const QString &)), hwnet, SLOT(SendTeamMessage(const QString &)));
+    connect(game, SIGNAL(SendDrawCmd(const QByteArray &)), hwnet, SLOT(SendDrawCmd(const QByteArray &)));
     connect(hwnet, SIGNAL(chatStringFromNet(const QString &)), game, SLOT(FromNetChat(const QString &)), Qt::QueuedConnection);
     connect(hwnet, SIGNAL(Warning(const QString&)), game, SLOT(FromNetWarning(const QString&)), Qt::QueuedConnection);
     connect(hwnet, SIGNAL(Error(const QString&)), game, SLOT(FromNetError(const QString&)), Qt::QueuedConnection);
--- a/QTfrontend/net/newnetclient.cpp	Thu Oct 06 20:58:54 2022 +0300
+++ b/QTfrontend/net/newnetclient.cpp	Sun Oct 16 13:14:16 2022 +0300
@@ -474,6 +474,29 @@
         return;
     }
 
+    if (netClientState == InRoom || netClientState == InGame || netClientState == InDemo)
+    {
+        if (lst[0] == "TEAMDRAW")
+        {
+            if(lst.size() < 3)
+            {
+                qWarning("Net: Empty CHAT message");
+                return;
+            }
+
+            QString action;
+            QString message = lst[2];
+            QByteArray sender = lst[1].toUtf8();
+
+            QByteArray em("Ou");
+            em.append(sender.size());
+            em.append(sender);
+            em.append(QByteArray::fromBase64(message.toLatin1()));
+            emit FromNet(em.prepend(em.size()));
+            return;
+        }
+    }
+
     if (lst[0] == "INFO")
     {
         if(lst.size() < 5)
@@ -1046,6 +1069,12 @@
     RawSendNet(QString("TEAMCHAT") + delimiter + str);
 }
 
+void HWNewNet::SendDrawCmd(const QByteArray& msg)
+{
+    QString str = QString(msg.toBase64());
+    RawSendNet(QString("TEAMDRAW") + delimiter + str);
+}
+
 void HWNewNet::askRoomsList()
 {
     if(netClientState != InLobby)
--- a/QTfrontend/net/newnetclient.h	Thu Oct 06 20:58:54 2022 +0300
+++ b/QTfrontend/net/newnetclient.h	Sun Oct 16 13:14:16 2022 +0300
@@ -153,6 +153,7 @@
         void chatLineToNetWithEcho(const QString&);
         void chatLineToLobby(const QString& str);
         void SendTeamMessage(const QString& str);
+        void SendDrawCmd(const QByteArray& msg);
         void SendNet(const QByteArray & buf);
         void AddTeam(const HWTeam & team);
         void RemoveTeam(const HWTeam& team);
--- a/doc/protocol.txt	Thu Oct 06 20:58:54 2022 +0300
+++ b/doc/protocol.txt	Sun Oct 16 13:14:16 2022 +0300
@@ -20,6 +20,7 @@
     't' + №         /taunt №
     'f' + <team>    'team' is uncontrolled
     'g' + <team>    'team' is controlled again (synced msg)
+    'O'             Drawing mode data
 
 фронтенд клиенту:
     'e' + <команда> выполнить "/<команда>"
--- a/gameServer/HWProtoInRoomState.hs	Thu Oct 06 20:58:54 2022 +0300
+++ b/gameServer/HWProtoInRoomState.hs	Sun Oct 16 13:14:16 2022 +0300
@@ -387,6 +387,12 @@
         engineMsg cl = toEngineMsg $ B.concat ["b", nick cl, "]", msg, "\x20\x20"]
 
 
+handleCmd_inRoom ["TEAMDRAW", msg] = do
+    cl <- thisClient
+    chans <- roomSameClanChans
+    return [AnswerClients chans ["TEAMDRAW", nick cl, msg]]
+
+
 handleCmd_inRoom ["BAN", banNick] = do
     (thisClientId, rnc) <- ask
     maybeClientId <- clientByNick banNick
--- a/hedgewars/CMakeLists.txt	Thu Oct 06 20:58:54 2022 +0300
+++ b/hedgewars/CMakeLists.txt	Sun Oct 16 13:14:16 2022 +0300
@@ -95,6 +95,7 @@
     uVisualGearsList.pas
     uVisualGearsHandlers.pas
     uVisualGears.pas
+    uDrawing.pas
 
     uGears.pas
     uGame.pas
--- a/hedgewars/hwengine.pas	Thu Oct 06 20:58:54 2022 +0300
+++ b/hedgewars/hwengine.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -32,7 +32,7 @@
 uses {$IFDEF IPHONEOS}cmem, {$ENDIF} SDLh, uMisc, uConsole, uGame, uConsts, uLand, uAmmos, uVisualGears, uGears, uStore, uWorld, uInputHandler
      , uSound, uScript, uTeams, uStats, uIO, uLocale, uChat, uAI, uAIMisc, uAILandMarks, uLandTexture, uCollisions
      , SysUtils, uTypes, uVariables, uCommands, uUtils, uCaptions, uDebug, uCommandHandlers, uLandPainted
-     , uPhysFSLayer, uCursor, uRandom, ArgParsers, uVisualGearsHandlers, uTextures, uRender
+     , uPhysFSLayer, uCursor, uRandom, ArgParsers, uVisualGearsHandlers, uTextures, uRender, uDrawing
      {$IFDEF USE_VIDEO_RECORDING}, uVideoRec {$ENDIF}
      {$IFDEF USE_TOUCH_INTERFACE}, uTouch {$ENDIF}
      {$IFDEF ANDROID}, GLUnit{$ENDIF}
@@ -207,12 +207,14 @@
                         SDL_WINDOWEVENT_FOCUS_GAINED:
                                 begin
                                 cHasFocus:= true;
-                                onFocusStateChanged();
+                                uWorld.onFocusStateChanged();
+                                uDrawing.onFocusStateChanged();
                                 end;
                         SDL_WINDOWEVENT_FOCUS_LOST:
                                 begin
                                 cHasFocus:= false;
-                                onFocusStateChanged();
+                                uWorld.onFocusStateChanged();
+                                uDrawing.onFocusStateChanged();
                                 end;
 {$IFDEF MOBILE}
 (* Suspend game if minimized on mobile.
@@ -541,6 +543,7 @@
         uTeams.initModule;
         uVisualGears.initModule;
         uVisualGearsHandlers.initModule;
+        uDrawing.initModule;
         uWorld.initModule;
     end;
 end;
@@ -555,6 +558,7 @@
         uAILandMarks.freeModule;
         uCaptions.freeModule;
         uWorld.freeModule;
+        uDrawing.freeModule;
         uVisualGears.freeModule;
         uTeams.freeModule;
         uInputHandler.freeModule;
--- a/hedgewars/uAIActions.pas	Thu Oct 06 20:58:54 2022 +0300
+++ b/hedgewars/uAIActions.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -246,7 +246,7 @@
                 ParseCommand('skip', true);
 
             aia_Put:
-                doPut(X, Y, true);
+                doPut(X, Y, true, false);
 
             aia_waitAngle:
                 if LongInt(Me^.Angle) <> Abs(Param) then exit;
--- a/hedgewars/uCommandHandlers.pas	Thu Oct 06 20:58:54 2022 +0300
+++ b/hedgewars/uCommandHandlers.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -27,7 +27,7 @@
 
 implementation
 uses uCommands, uTypes, uVariables, uIO, uDebug, uConsts, uScript, uUtils, SDLh, uWorld, uRandom, uCaptions
-    , uVisualGearsList, uGearsHedgehog
+    , uVisualGearsList, uGearsHedgehog, uDrawing
      {$IFDEF USE_VIDEO_RECORDING}, uVideoRec {$ENDIF};
 
 var cTagsMasks : array[0..15] of byte = (7, 0, 0, 0, 0, 4, 5, 6, 15, 8, 8, 8, 8, 12, 13, 14);
@@ -514,10 +514,20 @@
 PlayTaunt(byte(s[1]))
 end;
 
-procedure chPut(var s: shortstring);
+procedure chPut_p(var s: shortstring);
 begin
     s:= s; // avoid compiler hint
-    doPut(0, 0, false);
+    if uDrawing.isDrawingModeActive() then
+       uDrawing.onLeftMouseButtonPressed()
+    else
+       doPut(0, 0, false, false);
+end;
+
+procedure chPut_m(var s: shortstring);
+begin
+    s:= s; // avoid compiler hint
+    if uDrawing.isDrawingModeActive() then
+       uDrawing.onLeftMouseButtonReleased()
 end;
 
 procedure chCapture(var s: shortstring);
@@ -575,6 +585,13 @@
 procedure chAmmoMenu(var s: shortstring);
 begin
 s:= s; // avoid compiler hint
+
+if uDrawing.isDrawingModeActive() then
+begin
+   uDrawing.onRightMouseButtonPressed();
+   exit;
+end;
+
 if CheckNoTeamOrHH then
     bShowAmmoMenu:= (not bShowAmmoMenu)
 else
@@ -760,6 +777,11 @@
 procedure chZoomReset(var s: shortstring);
 begin
     s:= s; // avoid compiler hint
+    if uDrawing.isDrawingModeActive() then
+    begin
+       uDrawing.onMiddleMouseButtonPressed();
+       exit;
+    end;
     if (LocalMessage and gmPrecise <> 0) then
         ZoomValue:= cDefaultZoomLevel
     else
@@ -895,6 +917,18 @@
     end
 end;
 
+procedure chTeamDraw_p(var s: shortstring);
+begin
+   s:= s;
+   uDrawing.onModeButtonPressed();
+end;
+
+procedure chTeamDraw_m(var s: shortstring);
+begin
+   s:= s;
+   uDrawing.onModeButtonReleased();
+end;
+
 procedure chFastForward(var cmd: shortstring);
 var str0, str1, str2 : shortstring;
     h, m, s : integer;
@@ -1002,6 +1036,8 @@
     RegisterVariable('slot'    , @chSlot         , false);
     RegisterVariable('setweap' , @chSetWeapon    , false, true);
 //////// End top by freq analysis
+    RegisterVariable('+teamdraw', @chTeamDraw_p  , true);
+    RegisterVariable('-teamdraw', @chTeamDraw_m  , true);
     RegisterVariable('gencmd'  , @chGenCmd       , false);
     RegisterVariable('script'  , @chScript       , false);
     RegisterVariable('scriptparam', @chScriptParam, false);
@@ -1053,7 +1089,8 @@
     RegisterVariable('switch'  , @chSwitch       , false);
     RegisterVariable('timer'   , @chTimer        , false, true);
     RegisterVariable('taunt'   , @chTaunt        , false);
-    RegisterVariable('put'     , @chPut          , false);
+    RegisterVariable('+put'    , @chPut_p        , true);
+    RegisterVariable('-put'    , @chPut_m        , true);
     RegisterVariable('+volup'  , @chVolUp_p      , true );
     RegisterVariable('-volup'  , @chVolUp_m      , true );
     RegisterVariable('+voldown', @chVolDown_p    , true );
--- a/hedgewars/uCursor.pas	Thu Oct 06 20:58:54 2022 +0300
+++ b/hedgewars/uCursor.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -13,7 +13,7 @@
 
 implementation
 
-uses SDLh, uVariables, uTypes;
+uses SDLh, uVariables, uTypes, uDrawing;
 
 procedure init;
 begin
@@ -54,6 +54,7 @@
 begin
     CursorPoint.X:= CursorPoint.X + x;
     CursorPoint.Y:= CursorPoint.Y - y;
+    uDrawing.onCursorMoved();
 end;
 
 end.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hedgewars/uDrawing.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -0,0 +1,525 @@
+(*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2015 Andrey Korotaev <unC0Rr@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 2 of the License
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *)
+
+{$INCLUDE "options.inc"}
+
+unit uDrawing;
+(*
+ * This unit defines the Drawing mode, which allows drawing some graphics
+ * private to the members of a clan.
+ *)
+interface
+
+procedure initModule;
+procedure freeModule;
+
+function isDrawingModeActive(): boolean;
+procedure onModeButtonPressed();
+procedure onModeButtonReleased();
+procedure onFocusStateChanged();
+procedure onCursorMoved();
+procedure onLeftMouseButtonPressed();
+procedure onLeftMouseButtonReleased();
+procedure onRightMouseButtonPressed();
+procedure onMiddleMouseButtonPressed();
+procedure handleIPCInput(cmd : shortstring);
+
+implementation
+uses uTypes, uConsts, uVariables, uVisualGearsList, uUtils, uDebug, uIO, SDLh, Math;
+
+const
+   cColorsCount = 9;
+   cColors : array [0..cColorsCount - 1] of LongWord = (
+                                $ff020400,
+                                $4980c100,
+                                $1de6ba00,
+                                $b541ef00,
+                                $e55bb000,
+                                $20bf0000,
+                                $fe8b0e00,
+                                $8f590200,
+                                $ffff0100
+                                );
+   // Reserve one color for the local user
+   cKnownUsersMax = cColorsCount - 1;
+   cPointRadius = 25;
+   cBeaconDuration = 125;
+   cEffectDuration = 500;
+   cMaxDrawingRadius = 150;
+   cEffectGearsCountMax = 4;
+
+type
+   TDrawingState = (drwDisabled, drwStart, drwPoint, drwArrow);
+
+   TVisualEffect = record
+                      vGears     : array [0..cEffectGearsCountMax - 1] of PVisualGear;
+                      gearsCount : integer;
+                   end;
+   TDrawingContext = record
+                        state            : TDrawingState;
+                        currVEffect      : TVisualEffect;
+                        prevAutoCameraOn : boolean;
+                        startCursorX     : LongInt;
+                        startCursorY     : LongInt;
+                        knownUsers       : array [0..cKnownUsersMax - 1] of shortstring;
+                        knownUsersCount  : integer;
+                        lastReplacedUserIdx : integer;
+                     end;
+
+var drawingCtx : TDrawingContext;
+
+procedure AddKnownUser(user : shortstring);
+var i : integer;
+begin
+   with drawingCtx do
+   begin
+      for i:= 0 to knownUsersCount - 1 do
+      begin
+         if knownUsers[i] = user then
+            exit;
+      end;
+      if knownUsersCount < cKnownUsersMax then
+      begin
+         knownUsers[knownUsersCount]:= user;
+         Inc(knownUsersCount);
+      end
+      else
+      begin
+         lastReplacedUserIdx:= (lastReplacedUserIdx + 1) mod cKnownUsersMax;
+         knownUsers[lastReplacedUserIdx]:= user;
+      end;
+   end;
+end;
+
+function GetUserColor(user : shortstring) : LongWord;
+var i : integer;
+begin
+   if user = '' then  // local user
+      exit(cColors[0]);
+   with drawingCtx do
+   begin
+      for i:= 0 to knownUsersCount - 1 do
+      begin
+         if knownUsers[i] = user then
+         begin
+            exit(cColors[i + 1]);
+         end;
+      end;
+      exit(cColors[0]);
+   end;
+end;
+
+procedure recalcArrowParams(var arrow: TVisualEffect; X1, Y1, X2, Y2 : real);
+var tmp, tmpSin, tmpCos : real;
+begin
+   with arrow.vGears[0]^ do
+   begin
+      X:= X1;
+      Y:= Y1;
+      dX:= X2;
+      dY:= Y2;
+   end;
+   // Compute arrow pointer coordinates
+   if X2 = X1 then
+      if Y2 > Y1 then
+         tmp:= PI / 2
+      else
+         tmp:= -PI / 2
+   else
+      tmp:= arctan2(Y2 - Y1, X2 - X1);
+   tmpSin:= sin(tmp - PI / 4);
+   tmpCos:= cos(tmp - PI / 4);
+   with arrow.vGears[1]^ do
+   begin
+      X:= X2;
+      Y:= Y2;
+      dX:= X2 - 50 * tmpCos;
+      dY:= Y2 - 50 * tmpSin;
+   end;
+   tmpSin:= sin(tmp + PI / 4);
+   tmpCos:= cos(tmp + PI / 4);
+   with arrow.vGears[2]^ do
+   begin
+      X:= X2;
+      Y:= Y2;
+      dX:= X2 - 50 * tmpCos;
+      dY:= Y2 - 50 * tmpSin;
+   end;
+   // Compute circle center
+   with arrow.vGears[3]^ do
+   begin
+      X:= (X1 + X2) / 2;
+      Y:= (Y1 + Y2) / 2;
+   end;
+end;
+
+procedure doStepPoint(Gear: PVisualGear; Steps: Longword);
+var tmp: LongInt;
+begin
+if Gear^.FrameTicks <= Steps then
+   DeleteVisualGear(Gear)
+else
+begin
+   dec(Gear^.FrameTicks, Steps);
+   if Gear^.Tag = 0 then
+   begin
+      tmp:= round(Gear^.FrameTicks * $FF / cEffectDuration);
+      if tmp > $FF then
+         tmp:= $FF;
+      if tmp >= 0 then
+         Gear^.Tint:= (Gear^.Tint and $FFFFFF00) or Longword(tmp);
+   end
+   else if Gear^.Tag = 1 then
+   begin
+      Gear^.State:= round(Gear^.FrameTicks * 2048 / cBeaconDuration);
+   end;
+end;
+end;
+
+procedure doStepArrow(Gear: PVisualGear; Steps: Longword);
+var tmp: LongInt;
+begin
+if Gear^.Tag = 100 then
+   exit;
+if Gear^.FrameTicks <= Steps then
+   DeleteVisualGear(Gear)
+else
+begin
+   dec(Gear^.FrameTicks, Steps);
+   if Gear^.Tag < 3 then
+   begin
+      tmp:= round(Gear^.FrameTicks * $FF / cEffectDuration);
+      if tmp > $FF then
+         tmp:= $FF;
+      if tmp >= 0 then
+         Gear^.Tint:= (Gear^.Tint and $FFFFFF00) or Longword(tmp);
+   end
+   else if Gear^.Tag = 3 then
+   begin
+      Gear^.State:= round(Gear^.FrameTicks * 2048 / cBeaconDuration);
+   end;
+end;
+end;
+
+function isEffectEmpty(var vEffect : TVisualEffect) : boolean;
+begin
+   isEffectEmpty:= vEffect.gearsCount = 0;
+end;
+
+function AddVEffectCircle(user : shortstring; X, Y : LongInt) : TVisualEffect;
+var vGear  : PVisualGear;
+   vEffect : TVisualEffect;
+   color   : LongWord;
+   i       : integer;
+begin
+   color:= GetUserColor(user);
+   for i:= 0 to 1 do
+   begin
+      vGear := AddVisualGear(X, Y, vgtCircle, cPointRadius, true, 1);
+      if vGear = nil then
+      begin
+         OutError('uDrawing: AddVisualGear returned nil', false);
+         vEffect.gearsCount:= 0;
+         exit(vEffect);
+      end;
+      vGear^.Tint:= color or $FF;
+      vGear^.Angle:= 0;
+      vGear^.Timer:= 10;
+      vGear^.Tag:= i;
+      vGear^.doStep:= @doStepPoint;
+      vEffect.vGears[i]:= vGear;
+   end;
+   vEffect.vGears[0]^.Tint:= color or $FF;
+   vEffect.vGears[0]^.FrameTicks:= cEffectDuration;
+   vEffect.vGears[1]^.Tint:= color or $3F;
+   vEffect.vGears[1]^.FrameTicks:= cBeaconDuration;
+
+   vEffect.gearsCount:= 2;
+   AddVEffectCircle:= vEffect;
+end;
+
+function AddVEffectArrow(user : shortstring; X1, Y1, X2, Y2 : LongInt) : TVisualEffect;
+var vGear  : PVisualGear;
+   vEffect : TVisualEffect;
+   color   : LongWord;
+   i       : integer;
+begin
+   color:= GetUserColor(user);
+   for i:= 0 to 2 do
+   begin
+      vGear := AddVisualGear(0, 0, vgtLine, 10, true, 1);
+      if vGear = nil then
+      begin
+         OutError('uDrawing: AddVisualGear returned nil', false);
+         vEffect.gearsCount:= 0;
+         exit(vEffect);
+      end;
+      vGear^.Tint:= color or $FF;
+      vGear^.FrameTicks:= cEffectDuration;
+      vGear^.Tag:= 100;
+      vGear^.doStep:= @doStepArrow;
+      vEffect.vGears[i]:= vGear;
+   end;
+
+   vGear := AddVisualGear(0, 0, vgtCircle, 2048, true, 1);
+   if vGear = nil then
+   begin
+      OutError('uDrawing: AddVisualGear returned nil', false);
+      vEffect.gearsCount:= 0;
+      exit(vEffect);
+   end;
+   vGear^.Tint:= color;
+   vGear^.Angle:= 0;
+   vGear^.FrameTicks:= cBeaconDuration;
+   vGear^.Timer:= 10;
+   vGear^.Tag:= 100;
+   vGear^.doStep:= @doStepArrow;
+   vEffect.vGears[3]:= vGear;
+
+   vEffect.gearsCount:= 4;
+   recalcArrowParams(vEffect, X1, Y1, X2, Y2);
+
+   AddVEffectArrow:= vEffect;
+end;
+
+procedure VEffectArrowStart(var vEffect : TVisualEffect);
+var i : integer;
+begin
+   for i:= 0 to vEffect.gearsCount - 1 do
+      vEffect.vGears[i]^.Tag:= i;
+   vEffect.vGears[3]^.Tint:= (vEffect.vGears[3]^.Tint and $FFFFFF00) or $3F;
+end;
+
+procedure DeleteVEffect(var vEffect : TVisualEffect);
+var i : integer;
+begin
+   for i:= 0 to vEffect.gearsCount - 1 do
+      DeleteVisualGear(vEffect.vGears[i]);
+   vEffect.gearsCount:= 0;
+end;
+
+function isDrawingModeActive() : boolean;
+begin
+   isDrawingModeActive:= drawingCtx.state <> drwDisabled;
+end;
+
+procedure SendIPCArrow(X1, Y1, X2, Y2: LongInt);
+var s: shortstring;
+begin
+s[0]:= #18;
+s[1]:= 'O';
+s[2]:= 'a';
+SDLNet_Write32(X1, @s[3]);
+SDLNet_Write32(Y1, @s[7]);
+SDLNet_Write32(X2, @s[11]);
+SDLNet_Write32(Y2, @s[15]);
+SendIPC(s)
+end;
+
+procedure SendIPCCircle(X1, Y1: LongInt);
+var s: shortstring;
+begin
+s[0]:= #10;
+s[1]:= 'O';
+s[2]:= 'c';
+SDLNet_Write32(X1, @s[3]);
+SDLNet_Write32(Y1, @s[7]);
+SendIPC(s)
+end;
+
+procedure handleIPCInput(cmd: shortstring);
+var i, drwCmdOffset : integer;
+   userNameLen      : Byte;
+   user             : shortstring;
+   X1, Y1, X2, Y2   : LongInt;
+   VEffect          : TVisualEffect;
+begin
+case cmd[1] of
+  'u' : begin
+     userNameLen:= Byte(cmd[2]);
+     for i:= 0 to userNameLen do
+        user[i]:= cmd[2 + i];
+     drwCmdOffset:= 2 + userNameLen + 1;
+     if Length(cmd) < drwCmdOffset then
+        exit;
+     AddKnownUser(user);
+     case cmd[drwCmdOffset] of
+       'a' : begin
+          if Length(cmd) < drwCmdOffset + 4 * 4 then
+             exit;
+          X1:= SDLNet_Read32(@cmd[drwCmdOffset + 1]);
+          Y1:= SDLNet_Read32(@cmd[drwCmdOffset + 1 + 4]);
+          X2:= SDLNet_Read32(@cmd[drwCmdOffset + 1 + 8]);
+          Y2:= SDLNet_Read32(@cmd[drwCmdOffset + 1 + 12]);
+          VEffect:= AddVEffectArrow(user, X1, Y1, X2, Y2);
+          if not isEffectEmpty(VEffect) then
+             VEffectArrowStart(VEffect);
+       end;
+       'c' : begin
+          if Length(cmd) < drwCmdOffset + 4 * 2 then
+             exit;
+          X1:= SDLNet_Read32(@cmd[drwCmdOffset + 1]);
+          Y1:= SDLNet_Read32(@cmd[drwCmdOffset + 1 + 4]);
+          VEffect:= AddVEffectCircle(user, X1, Y1);
+       end;
+     end;
+  end;
+end;
+end;
+
+procedure onModeButtonPressed();
+begin
+   drawingCtx.state:= drwStart;
+   drawingCtx.prevAutoCameraOn:= autoCameraOn;
+   autoCameraOn:= false;
+end;
+
+procedure onModeButtonReleased();
+begin
+   DeleteVEffect(drawingCtx.currVEffect);
+   if drawingCtx.state <> drwDisabled then
+   begin
+      drawingCtx.state:= drwDisabled;
+      autoCameraOn:= drawingCtx.prevAutoCameraOn;
+   end;
+end;
+
+procedure onFocusStateChanged();
+begin
+   if not cHasFocus then
+      onModeButtonReleased();
+end;
+
+procedure onLeftMouseButtonPressed();
+begin
+if not isDrawingModeActive() then
+   exit;
+case drawingCtx.state of
+    drwStart: begin
+                 drawingCtx.startCursorX:= CursorPoint.X;
+                 drawingCtx.startCursorY:= CursorPoint.Y;
+                 drawingCtx.state:= drwPoint;
+              end;
+end;
+end;
+
+procedure onLeftMouseButtonReleased();
+var tmpX, tmpY, tmpX2, tmpY2 : LongInt;
+   vEffect     : TVisualEffect;
+begin
+if not isDrawingModeActive() then
+   exit;
+case drawingCtx.state of
+    drwPoint: begin
+                 tmpX:= drawingCtx.startCursorX - WorldDx;
+                 tmpY:= cScreenHeight - drawingCtx.startCursorY - WorldDy;
+                 vEffect:= AddVEffectCircle('', tmpX, tmpY);
+                 if not isEffectEmpty(vEffect) then
+                    SendIPCCircle(tmpX, tmpY);
+                 drawingCtx.state:= drwStart;
+              end;
+    drwArrow: begin
+                  tmpX2:= CursorPoint.X - WorldDx;
+                  tmpY2:= cScreenHeight - CursorPoint.Y - WorldDy;
+                  with drawingCtx do
+                  begin
+                     tmpX:= startCursorX - WorldDx;
+                     tmpY:= cScreenHeight - startCursorY - WorldDy;
+                     recalcArrowParams(currVEffect, tmpX, tmpY, tmpX2, tmpY2);
+                     VEffectArrowStart(currVEffect);
+                     SendIPCArrow(tmpX, tmpY, tmpX2, tmpY2);
+                     currVEffect.gearsCount:= 0;
+                  end;
+                  drawingCtx.state:= drwStart;
+               end;
+end;
+end;
+
+procedure onRightMouseButtonPressed();
+begin
+   if not isDrawingModeActive() then
+      exit;
+   DeleteVEffect(drawingCtx.currVEffect);
+   drawingCtx.state:= drwStart;
+end;
+
+procedure onMiddleMouseButtonPressed();
+begin
+end;
+
+procedure onCursorMoved();
+var tmpX, tmpY, tmpX2, tmpY2, dX, dY : LongInt;
+   h                                 : real;
+begin
+if not isDrawingModeActive() then
+   exit;
+autoCameraOn:= false;
+dX:= CursorPoint.X - drawingCtx.startCursorX;
+dY:= CursorPoint.Y - drawingCtx.startCursorY;
+h:= sqrt(dX * dX + dY * dY);
+if (drawingCtx.state <> drwStart) and (h > cMaxDrawingRadius) then
+begin
+   CursorPoint.X:= drawingCtx.startCursorX + round(dX * cMaxDrawingRadius / h);
+   CursorPoint.Y:= drawingCtx.startCursorY + round(dY * cMaxDrawingRadius / h);
+end;
+case drawingCtx.state of
+    drwPoint : begin
+                 if h > cPointRadius then
+                 begin
+                    tmpX:= drawingCtx.startCursorX - WorldDx;
+                    tmpY:= cScreenHeight - drawingCtx.startCursorY - WorldDy;
+                    tmpX2:= CursorPoint.X - WorldDx;
+                    tmpY2:= cScreenHeight - CursorPoint.Y - WorldDy;
+                    drawingCtx.currVEffect:= AddVEffectArrow('', tmpX, tmpY, tmpX2, tmpY2);
+                    if not isEffectEmpty(drawingCtx.currVEffect) then
+                       drawingCtx.state:= drwArrow
+                    else
+                       drawingCtx.state:= drwStart;
+                 end;
+              end;
+    drwArrow : begin
+                  tmpX:= drawingCtx.startCursorX - WorldDx;
+                  tmpY:= cScreenHeight - drawingCtx.startCursorY - WorldDy;
+                  tmpX2:= CursorPoint.X - WorldDx;
+                  tmpY2:= cScreenHeight - CursorPoint.Y - WorldDy;
+                  with drawingCtx do
+                  begin
+                     recalcArrowParams(currVEffect, tmpX, tmpY, tmpX2, tmpY2);
+                  end;
+               end;
+end;
+end;
+
+procedure initModule;
+begin
+   with drawingCtx do
+   begin
+      state:= drwDisabled;
+      currVEffect.gearsCount:= 0;
+      startCursorX:= 0;
+      startCursorY:= 0;
+      knownUsersCount:= 0;
+      lastReplacedUserIdx:= 0;
+   end;
+end;
+
+procedure freeModule;
+begin
+end;
+
+end.
--- a/hedgewars/uIO.pas	Thu Oct 06 20:58:54 2022 +0300
+++ b/hedgewars/uIO.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -36,10 +36,10 @@
 procedure IPCWaitPongEvent;
 procedure IPCCheckSock;
 procedure NetGetNextCmd;
-procedure doPut(putX, putY: LongInt; fromAI: boolean);
+procedure doPut(putX, putY: LongInt; fromAI, extSource: boolean);
 
 implementation
-uses uConsole, uConsts, uVariables, uCommands, uUtils, uDebug, uLocale, uSound;
+uses uConsole, uConsts, uVariables, uCommands, uUtils, uDebug, uLocale, uSound, uDrawing;
 
 const
     cSendEmptyPacketTime = 1000;
@@ -207,6 +207,10 @@
              end
           else
              isProcessed:= false;
+     'O': begin
+              s:= copy(s, 2, Length(s) - 1);
+              uDrawing.handleIPCInput(s);
+          end;
      else
         isProcessed:= false;
      end;
@@ -443,7 +447,7 @@
         'p': begin
             x32:= SDLNet_Read32(@(headcmd^.str[2]));
             y32:= SDLNet_Read32(@(headcmd^.str[6]));
-            doPut(x32, y32, false)
+            doPut(x32, y32, false, true)
              end;
         'P': begin
             // these are equations solved for CursorPoint
@@ -498,9 +502,10 @@
         halt(HaltFatalErrorNoIPC);
 end;
 
-procedure doPut(putX, putY: LongInt; fromAI: boolean);
+procedure doPut(putX, putY: LongInt; fromAI, extSource: boolean);
 begin
-if CheckNoTeamOrHH or isPaused then
+if CheckNoTeamOrHH or isPaused or (CurrentTeam^.ExtDriven and (not extSource)) or
+   (CurrentHedgehog = nil) or ((CurrentHedgehog^.BotLevel <> 0) and (not fromAI)) then
     exit;
 bShowFinger:= false;
 if (not CurrentTeam^.ExtDriven) and bShowAmmoMenu then
--- a/hedgewars/uInputHandler.pas	Thu Oct 06 20:58:54 2022 +0300
+++ b/hedgewars/uInputHandler.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -415,6 +415,7 @@
     RegisterBind(DefaultBinds, _S't', 'chat');
     RegisterBind(DefaultBinds, _S'u', 'chat team');
     RegisterBind(DefaultBinds, _S'y', 'confirm');
+    RegisterBind(DefaultBinds, _S'd', '+teamdraw');
 
     RegisterBind(DefaultBinds, 'mousem', 'zoomreset');
     RegisterBind(DefaultBinds, 'wheelup', 'zoomin');
@@ -426,7 +427,7 @@
     for i:= 1 to 5  do RegisterBind(DefaultBinds, IntToStr(i), 'timer '+IntToStr(i));
     RegisterBind(DefaultBinds, _S'n', 'timer_u');
 
-    RegisterBind(DefaultBinds, 'mousel', '/put');
+    RegisterBind(DefaultBinds, 'mousel', '+put');
     RegisterBind(DefaultBinds, 'mouser', 'ammomenu');
     RegisterBind(DefaultBinds, 'backspace', 'hjump');
     RegisterBind(DefaultBinds, 'tab', 'switch');
--- a/hedgewars/uTouch.pas	Thu Oct 06 20:58:54 2022 +0300
+++ b/hedgewars/uTouch.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -309,7 +309,7 @@
         if (CurrentHedgehog <> nil)then
             if(Ammoz[CurrentHedgehog^.CurAmmoType].Ammo.Propz and ammoprop_NeedTarget <> 0)then
                 begin
-                ParseTeamCommand('put');
+                ParseTeamCommand('+put');
                 targetted:= true;
                 end
             else if (CurAmmoGear <> nil) and (CurAmmoGear^.AmmoType = amSwitch) then
@@ -346,7 +346,7 @@
         begin
         CursorPoint.X:= finger.x;
         CursorPoint.Y:= finger.y;
-        ParseTeamCommand('put');
+        ParseTeamCommand('+put');
         end
     else
         bShowAmmoMenu:= false;
--- a/hedgewars/uTypes.pas	Thu Oct 06 20:58:54 2022 +0300
+++ b/hedgewars/uTypes.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -121,7 +121,7 @@
             vgtDust, vgtSplash, vgtDroplet, vgtSmokeRing, vgtBeeTrace, vgtEgg,
             vgtFeather, vgtHealthTag, vgtSmokeTrace, vgtEvilTrace, vgtExplosion,
             vgtBigExplosion, vgtChunk, vgtNote, vgtLineTrail, vgtBulletHit, vgtCircle,
-            vgtSmoothWindBar, vgtStraightShot, vgtNoPlaceWarn);
+            vgtSmoothWindBar, vgtStraightShot, vgtNoPlaceWarn, vgtLine);
 
     // Damage can be caused by different sources
     TDamageSource = (dsUnknown, dsFall, dsBullet, dsExplosion, dsShove, dsPoison, dsHammer);
--- a/hedgewars/uVisualGears.pas	Thu Oct 06 20:58:54 2022 +0300
+++ b/hedgewars/uVisualGears.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -138,6 +138,18 @@
     exit(@SpritesData[GetSprite(sprite, SDsprite)]);
 end;
 
+procedure DrawCircleGear(gear : PVisualGear);
+var tmp: real;
+begin
+   if gear^.Angle = 1 then
+   begin
+      tmp:= Gear^.State / 100;
+      DrawTexture(round(Gear^.X-24*tmp) + WorldDx, round(Gear^.Y-24*tmp) + WorldDy, SpritesData[sprVampiric].Texture, tmp)
+   end
+   else
+      DrawCircle(round(Gear^.X) + WorldDx, round(Gear^.Y) + WorldDy, Gear^.State, Gear^.Timer);
+end;
+
 procedure DrawVisualGears(Layer: LongWord; worldIsShifted: boolean);
 var Gear: PVisualGear;
     tinted, speedlessFlakes: boolean;
@@ -208,6 +220,8 @@
               vgtEvilTrace: if Gear^.State < 8 then
                   DrawSprite(sprEvilTrace, round(Gear^.X) + WorldDx, round(Gear^.Y) + WorldDy, Gear^.State);
               vgtLineTrail: DrawLine(Gear^.X, Gear^.Y, Gear^.dX, Gear^.dY, 1.0, $FF, min(Gear^.Timer, $C0), min(Gear^.Timer, $80), min(Gear^.Timer, (Gear^.Tint and $FF)));
+              vgtLine: DrawLine(Gear^.X, Gear^.Y, Gear^.dX, Gear^.dY, Gear^.State, Gear^.Tint);
+              vgtCircle: DrawCircleGear(gear);
           end;
           if (cReducedQuality and rqAntiBoom) = 0 then
               case Gear^.Kind of
@@ -377,13 +391,7 @@
                              else
                                  DrawTextureRotatedF(spriteData^.Texture, Gear^.Scale, 0, 0, round(Gear^.X) + WorldDx, round(Gear^.Y) + WorldDy + SkyOffset, Gear^.Frame, 1, spriteData^.Width, spriteData^.Height, Gear^.Angle);
                              end;
-               vgtCircle: if gear^.Angle = 1 then
-                              begin
-                              tmp:= Gear^.State / 100;
-                              DrawTexture(round(Gear^.X-24*tmp) + WorldDx, round(Gear^.Y-24*tmp) + WorldDy, SpritesData[sprVampiric].Texture, tmp)
-                              end
-                          else
-                              DrawCircle(round(Gear^.X) + WorldDx, round(Gear^.Y) + WorldDy, Gear^.State, Gear^.Timer);
+              vgtCircle: DrawCircleGear(gear);
            end;
            if (Gear^.Tint <> $FFFFFFFF) or tinted then
                untint;
--- a/hedgewars/uVisualGearsHandlers.pas	Thu Oct 06 20:58:54 2022 +0300
+++ b/hedgewars/uVisualGearsHandlers.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -335,6 +335,18 @@
 end;
 
 ////////////////////////////////////////////////////////////////////////////////
+procedure doStepLine(Gear: PVisualGear; Steps: Longword);
+begin
+{$IFNDEF PAS2C}
+Steps := Steps;
+{$ENDIF}
+if Gear^.Timer <= Steps then
+    DeleteVisualGear(Gear)
+else
+    dec(Gear^.Timer, Steps)
+end;
+
+////////////////////////////////////////////////////////////////////////////////
 procedure doStepEgg(Gear: PVisualGear; Steps: Longword);
 begin
 Gear^.X:= Gear^.X + Gear^.dX * Steps;
@@ -1072,7 +1084,8 @@
             @doStepCircle,
             @doStepSmoothWindBar,
             @doStepStraightShot,
-            @doStepNoPlaceWarn
+            @doStepNoPlaceWarn,
+            @doStepLine
         );
 
 procedure initModule;
--- a/hedgewars/uVisualGearsList.pas	Thu Oct 06 20:58:54 2022 +0300
+++ b/hedgewars/uVisualGearsList.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -403,6 +403,7 @@
     vgtSmokeTrace,
     vgtEvilTrace,
     vgtLineTrail,
+    vgtLine,
     vgtSmoke,
     vgtSmokeWhite,
     vgtDust,
--- a/hedgewars/uWorld.pas	Thu Oct 06 20:58:54 2022 +0300
+++ b/hedgewars/uWorld.pas	Sun Oct 16 13:14:16 2022 +0300
@@ -65,6 +65,7 @@
     , uTeams
     , uDebug
     , uInputHandler
+    , uDrawing
 {$IFDEF USE_VIDEO_RECORDING}
     , uVideoRec
 {$ENDIF}
@@ -1925,6 +1926,9 @@
     DrawTextureF(SpritesData[sprArrow].Texture, cDefaultZoomLevel / cScaleFactor, TargetCursorPoint.X + round(SpritesData[sprArrow].Width / cScaleFactor), cScreenHeight + round(SpritesData[sprArrow].Height / cScaleFactor) - TargetCursorPoint.Y, (RealTicks shr 6) mod 8, 1, SpritesData[sprArrow].Width, SpritesData[sprArrow].Height);
     end;
 
+if uDrawing.isDrawingModeActive() then
+   DrawTextureF(SpritesData[sprArrow].Texture, cDefaultZoomLevel / cScaleFactor, CursorPoint.X + round(SpritesData[sprArrow].Width / cScaleFactor), cScreenHeight + round(SpritesData[sprArrow].Height / cScaleFactor) - CursorPoint.Y, (RealTicks shr 6) mod 8, 1, SpritesData[sprArrow].Width, SpritesData[sprArrow].Height);
+
 // debug stuff
 if cViewLimitsDebug then
     begin
@@ -2010,9 +2014,9 @@
     exit
 end;
 
-if isCursorVisible then
+if isCursorVisible or uDrawing.isDrawingModeActive() then
     begin
-    if (not CurrentTeam^.ExtDriven) and (GameTicks >= PrevSentPointTime + cSendCursorPosTime) then
+    if isCursorVisible and (not CurrentTeam^.ExtDriven) and (GameTicks >= PrevSentPointTime + cSendCursorPosTime) then
         begin
         SendIPCXY('P', CursorPoint.X - WorldDx, cScreenHeight - CursorPoint.Y - WorldDy);
         PrevSentPointTime:= GameTicks
@@ -2024,7 +2028,8 @@
 
 // this generates the border around the screen that moves the camera when cursor is near it
 if (CurrentTeam^.ExtDriven and isCursorVisible and autoCameraOn) or
-   (not CurrentTeam^.ExtDriven and isCursorVisible) or ((FollowGear <> nil) and autoCameraOn) then
+   (not CurrentTeam^.ExtDriven and isCursorVisible) or
+   ((FollowGear <> nil) and autoCameraOn) or uDrawing.isDrawingModeActive() then
     begin
     if CursorPoint.X < - trunc(cScreenWidth / cScaleFactor) + EdgesDist then
         begin