PvP needs spell batching or the meta is tarnished

Feel free to find any change where they actually balanced anything around mutually exclusive spells going off at the same time.

Other than making it harder and harder to do as server resources improved before ultimately just killing it altogether.

4 Likes

It’s not about what they changed, it’s about what they didn’t change. They kept the batching for 10 years because everything was designed around it, and when they changed it in Warlords of Draenor (great expansion by the way) they formally addressed the way in which it would affect theorycrafting in a giant megathread that you can google an archive for if you’re so inclined.

24 Likes

Yes the server architecture was designed around it and it took some massive changes there to fix it. That doesn’t mean gameplay was designed around or that it was ever intended.

4 Likes

It’s the same thing. It was the reality of how the game was played. It doesn’t matter if it was code or network or something else. It’s how it was.

You can ask the original devs what their vision was for the game, and they’ll tell you 1000 other things they wanted to play out that didn’t. So with your argument should they be in Classic also?

14 Likes

#nochanges includes old server spell batch processing

27 Likes

And we already know we’re getting modern server structure so…

6 Likes

It doesn’t matter if it was intentional if it affects meta/balance. I’m sympathetic to improving latency/responsiveness but it has nothing to do with that.

Did anybody notice a problem with batching during MoP/Cata?

15 Likes

I haven’t given up hope!

Players loved Vanilla the way it was. Yes it had loads of problems of which some would say spell batching was one. But if Blizzard just trusts in Vanilla. Classic can turn out fine. It doesn’t have to be different.

Trust in Vanilla.

19 Likes

Then think about it like this, they’ve successfully reduced batch sizes down to one spell.

3 Likes

Is it known, and acknowledged by Blizzard, that the spell batch processing is incompatible (and not able to be simulated in some not-perfect-but-perhaps-good-enough way) with the modern server structure?

No, they haven’t commented on it at all. But we know that it wasn’t in the demo. And again, it has nothing to do with latency/responsiveness - MoP/Cata had spell batching and those games were plenty responsive.

Look, here’s the thing. As an example, replicating the old graphics as “pixel perfect” is compatible but they said during blizzcon that they wouldn’t be striving for that, which I don’t like but it’s reasonable, but I could easily see them cutting corners in that same way for spell batching since it only affects high end PvP and therefore not the majority.

21 Likes

Hmm. You know, when I was talking to my friends about Classic(who happen to all play Rogues), Vanish-immuning CC was a thing we brought up about being excited about. It didn’t really occur to me until now that it possibly wont be in Classic. That would be kind of depressing.

27 Likes

wouldn’t be that hard to replicate considering there’s plenty of emulation code for it that random people come up with for private servers

diff --git a/src/server/game/Entities/Player/ActionBatchObject.cpp b/src/server/game/Entities/Player/ActionBatchObject.cpp
new file mode 100644
index 0000000000..dfefef657a
--- /dev/null
+++ b/src/server/game/Entities/Player/ActionBatchObject.cpp
@@ -0,0 +1,126 @@
+/*
+* Copyright (C) 2008-2018 TrinityCore <https://www.trinitycore.org/>
+*
+* 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; either version 2 of the License, or (at your
+* option) any later version.
+*
+* 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, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "ActionBatchObject.h"
+#include "SpellInfo.h"
+#include "SpellMgr.h"
+#include "WorldSession.h"
+
+ActionBatchObject::ActionBatchObject(Player* owner) : m_owner(owner)
+{
+}
+
+void ActionBatchObject::CreateBatchObject(WorldPacket& data)
+{
+    if (IsPacketBatchable(data)) // checking for some special cases (eg. self casts)
+        m_packetBatch.push(data);
+}
+
+void ActionBatchObject::ProcessBatchedObjects()
+{
+    while (!m_packetBatch.empty())
+    {
+        WorldPacket data = m_packetBatch.front();
+
+        WorldSession* session = m_owner->GetSession();
+
+        switch (data.GetOpcode())
+        {
+            case CMSG_CAST_SPELL:
+                session->HandleCastSpellOpcode(data);
+                break;
+            case CMSG_CANCEL_CAST:
+                session->HandleCancelCastOpcode(data);
+                break;
+            case CMSG_CANCEL_AURA:
+                session->HandleCancelAuraOpcode(data);
+                break;
+            case CMSG_CANCEL_AUTO_REPEAT_SPELL:
+                session->HandleCancelAutoRepeatSpellOpcode(data);
+                break;
+            case CMSG_CANCEL_CHANNELLING:
+                session->HandleCancelChanneling(data);
+                break;
+            case CMSG_CANCEL_GROWTH_AURA:
+                session->HandleCancelGrowthAuraOpcode(data);
+                break;
+            case CMSG_CANCEL_MOUNT_AURA:
+                session->HandleCancelMountAuraOpcode(data);
+                break;
+            case CMSG_LOOT:
+                session->HandleLootOpcode(data);
+                break;
+            case CMSG_LOOT_CURRENCY:
+                session->HandleLootCurrencyOpcode(data);
+                break;
+            case CMSG_LOOT_MASTER_GIVE:
+                session->HandleLootMasterGiveOpcode(data);
+                break;
+            case CMSG_LOOT_METHOD:
+                session->HandleLootMethodOpcode(data);
+                break;
+            case CMSG_LOOT_MONEY:
+                session->HandleLootMoneyOpcode(data);
+                break;
+            case CMSG_LOOT_RELEASE:
+                session->HandleLootReleaseOpcode(data);
+                break;
+            case CMSG_LOOT_ROLL:
+                session->HandleLootRoll(data);
+                break;
+            default:
+                break;
+        }
+        m_packetBatch.pop();
+    }
+}
+
+bool ActionBatchObject::IsPacketBatchable(WorldPacket& data) const
+{
+    switch (data.GetOpcode())
+    {
+        case CMSG_CAST_SPELL:
+        {
+            if (m_owner->m_unitMovedByMe != m_owner) // vehicle casts and mind controls are also getting batched
+                return true;
+
+            // only reading the spell targets for now
+            data.read_skip<uint8>();  // cast count
+            data.read_skip<uint32>(); // spell Id
+            data.read_skip<uint32>(); // glyph index
+            data.read_skip<uint8>();  // cast flags
+            SpellCastTargets targets;
+            targets.Read(data, m_owner);
+            data.rfinish();
+
+            // if we target ourself the cast will be instant. Otherwise it will be batched
+            if (targets.GetUnitTarget() && targets.GetUnitTarget() == m_owner)
+            {
+                if (WorldSession* session = m_owner->GetSession())
+                    session->HandleCastSpellOpcode(data);
+
+                return false;
+            }
+
+            return true;
+        }
+        default:
+            return true;
+    }
+
+    return true;
+}
diff --git a/src/server/game/Entities/Player/ActionBatchObject.h b/src/server/game/Entities/Player/ActionBatchObject.h
new file mode 100644
index 0000000000..2fb7b57bd6
--- /dev/null
+++ b/src/server/game/Entities/Player/ActionBatchObject.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008-2018 TrinityCore <https://www.trinitycore.org/>
+ *
+ * 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; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef ActionBatchObject_h__
+#define ActionBatchObject_h__
+
+#include "Define.h"
+#include "Spell.h"
+#include "WorldPacket.h"
+
+class TC_GAME_API ActionBatchObject
+{
+    public:
+        ActionBatchObject(Player* owner);
+
+        void CreateBatchObject(WorldPacket& data);
+        void ProcessBatchedObjects();
+
+        bool IsPacketBatchable(WorldPacket& data) const;
+
+    private:
+        // Packet storage which contains all stored up packets
+        std::queue<WorldPacket> m_packetBatch;
+
+        // Player owner who is going to process all batched up packets
+        Player* m_owner;
+};
+
+#endif // ActionBatchObject_h__
diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp
index df7d7e481f..1a8406c819 100644
--- a/src/server/game/Entities/Player/Player.cpp
+++ b/src/server/game/Entities/Player/Player.cpp
@@ -19,6 +19,7 @@
 #include "Player.h"
 #include "AccountMgr.h"
 #include "AchievementMgr.h"
+#include "ActionBatchObject.h"
 #include "Archaeology.h"
 #include "ArenaTeam.h"
 #include "ArenaTeamMgr.h"
@@ -413,6 +414,7 @@ Player::Player(WorldSession* session): Unit(true)
     m_reputationMgr = new ReputationMgr(this);
     _hasValidLFGLeavePoint = false;
     _archaeology = new Archaeology(this);
+    m_actionBatchObjects = new ActionBatchObject(this);
 }
 
 Player::~Player()
@@ -447,6 +449,7 @@ Player::~Player()
     delete m_reputationMgr;
     delete _cinematicMgr;
     delete _archaeology;
+    delete m_actionBatchObjects;
 
     for (uint8 i = 0; i < VOID_STORAGE_MAX_SLOT; ++i)
         delete _voidStorageItems[i];
@@ -1388,6 +1391,12 @@ void Player::Update(uint32 p_time)
     if (IsHasDelayedTeleport() && IsAlive())
         TeleportTo(m_teleport_dest, m_teleport_options);
 
+    m_batchProcessingTimer.Update(p_time);
+    if (m_batchProcessingTimer.Passed())
+    {
+        m_actionBatchObjects->ProcessBatchedObjects();
+        m_batchProcessingTimer.Reset(400); // confirmed by blueposts
+    }
 }
 
 void Player::setDeathState(DeathState s)
@@ -28412,3 +28421,8 @@ void Player::SendTamePetFailure(PetTameFailureReason reason)
     data << uint8(reason);
     SendDirectMessage(&data);
 }
+
+void Player::AddBatchAction(WorldPacket& packet)
+{
+    m_actionBatchObjects->CreateBatchObject(packet);
+}
diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h
index da89290b97..f1d5ec6dfc 100644
--- a/src/server/game/Entities/Player/Player.h
+++ b/src/server/game/Entities/Player/Player.h
@@ -56,7 +56,7 @@ struct VendorItem;
 
 template<class T>
 class AchievementMgr;
-
+class ActionBatchObject;
 class Archaeology;
 class Bag;
 class Battleground;
@@ -2373,6 +2373,9 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
         void DeleteFromPlayerPetDataStore(uint32 petNumber);
         void AddToPlayerPetDataStore(PlayerPetData* playerPetData);
 
+        // Action Batching
+        void AddBatchAction(WorldPacket& packet);
+
     protected:
         // Gamemaster whisper whitelist
         GuidList WhisperList;
@@ -2751,6 +2754,10 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
         Archaeology* _archaeology;
 
         std::vector<PlayerPetData*> PlayerPetDataStore;
+
+        // Action batching system
+        TimeTrackerSmall m_batchProcessingTimer;
+        ActionBatchObject* m_actionBatchObjects;
 };
 
 TC_GAME_API void AddItemsSetItem(Player* player, Item* item);
diff --git a/src/server/game/Handlers/MiscHandler.cpp b/src/server/game/Handlers/MiscHandler.cpp
index c9097271e9..de629d7625 100644
--- a/src/server/game/Handlers/MiscHandler.cpp
+++ b/src/server/game/Handlers/MiscHandler.cpp
@@ -2061,3 +2061,8 @@ void WorldSession::HandleRequestResearchHistory(WorldPacket & /*recv_data*/)
     if (Player* player = GetPlayer())
         player->NotifyRequestResearchHistory();
 }
+
+void WorldSession::HandleBatchedAction(WorldPacket& recv_data)
+{
+    _player->AddBatchAction(recv_data);
+}
diff --git a/src/server/game/Server/Protocol/Opcodes.cpp b/src/server/game/Server/Protocol/Opcodes.cpp
index 9df9b15eeb..7399d9fa4a 100644
--- a/src/server/game/Server/Protocol/Opcodes.cpp
+++ b/src/server/game/Server/Protocol/Opcodes.cpp
@@ -147,16 +147,16 @@ void OpcodeTable::Initialize()
     DEFINE_OPCODE_HANDLER(CMSG_CALENDAR_GUILD_FILTER,                            STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleCalendarGuildFilter       );
     DEFINE_OPCODE_HANDLER(CMSG_CALENDAR_REMOVE_EVENT,                            STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleCalendarRemoveEvent       );
     DEFINE_OPCODE_HANDLER(CMSG_CALENDAR_UPDATE_EVENT,                            STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleCalendarUpdateEvent       );
-    DEFINE_OPCODE_HANDLER(CMSG_CANCEL_AURA,                                      STATUS_LOGGEDIN,  PROCESS_INPLACE,      &WorldSession::HandleCancelAuraOpcode          );
-    DEFINE_OPCODE_HANDLER(CMSG_CANCEL_AUTO_REPEAT_SPELL,                         STATUS_LOGGEDIN,  PROCESS_INPLACE,      &WorldSession::HandleCancelAutoRepeatSpellOpcode);
-    DEFINE_OPCODE_HANDLER(CMSG_CANCEL_CAST,                                      STATUS_LOGGEDIN,  PROCESS_THREADSAFE,   &WorldSession::HandleCancelCastOpcode          );
-    DEFINE_OPCODE_HANDLER(CMSG_CANCEL_CHANNELLING,                               STATUS_LOGGEDIN,  PROCESS_INPLACE,      &WorldSession::HandleCancelChanneling          );
-    DEFINE_OPCODE_HANDLER(CMSG_CANCEL_GROWTH_AURA,                               STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleCancelGrowthAuraOpcode    );
-    DEFINE_OPCODE_HANDLER(CMSG_CANCEL_MOUNT_AURA,                                STATUS_LOGGEDIN,  PROCESS_INPLACE,      &WorldSession::HandleCancelMountAuraOpcode     );
+    DEFINE_OPCODE_HANDLER(CMSG_CANCEL_AURA,                                      STATUS_LOGGEDIN,  PROCESS_INPLACE,      &WorldSession::HandleBatchedAction             );
+    DEFINE_OPCODE_HANDLER(CMSG_CANCEL_AUTO_REPEAT_SPELL,                         STATUS_LOGGEDIN,  PROCESS_INPLACE,      &WorldSession::HandleBatchedAction             );
+    DEFINE_OPCODE_HANDLER(CMSG_CANCEL_CAST,                                      STATUS_LOGGEDIN,  PROCESS_THREADSAFE,   &WorldSession::HandleBatchedAction             );
+    DEFINE_OPCODE_HANDLER(CMSG_CANCEL_CHANNELLING,                               STATUS_LOGGEDIN,  PROCESS_INPLACE,      &WorldSession::HandleBatchedAction             );
+    DEFINE_OPCODE_HANDLER(CMSG_CANCEL_GROWTH_AURA,                               STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleBatchedAction             );
+    DEFINE_OPCODE_HANDLER(CMSG_CANCEL_MOUNT_AURA,                                STATUS_LOGGEDIN,  PROCESS_INPLACE,      &WorldSession::HandleBatchedAction             );
     DEFINE_OPCODE_HANDLER(CMSG_CANCEL_QUEUED_SPELL,                              STATUS_UNHANDLED, PROCESS_INPLACE,      &WorldSession::Handle_NULL                     );
     DEFINE_OPCODE_HANDLER(CMSG_CANCEL_TEMP_ENCHANTMENT,                          STATUS_LOGGEDIN,  PROCESS_INPLACE,      &WorldSession::HandleCancelTempEnchantmentOpcode);
     DEFINE_OPCODE_HANDLER(CMSG_CANCEL_TRADE,                                     STATUS_LOGGEDIN_OR_RECENTLY_LOGGOUT, PROCESS_THREADUNSAFE, &WorldSession::HandleCancelTradeOpcode);
-    DEFINE_OPCODE_HANDLER(CMSG_CAST_SPELL,                                       STATUS_LOGGEDIN,  PROCESS_THREADSAFE,   &WorldSession::HandleCastSpellOpcode           );
+    DEFINE_OPCODE_HANDLER(CMSG_CAST_SPELL,                                       STATUS_LOGGEDIN,  PROCESS_THREADSAFE,   &WorldSession::HandleBatchedAction             );
     DEFINE_OPCODE_HANDLER(CMSG_CHANGEPLAYER_DIFFICULTY,                          STATUS_LOGGEDIN,  PROCESS_THREADSAFE,   &WorldSession::HandleChangePlayerDifficulty    );
     DEFINE_OPCODE_HANDLER(CMSG_CHANGE_SEATS_ON_CONTROLLED_VEHICLE,               STATUS_LOGGEDIN,  PROCESS_INPLACE,      &WorldSession::HandleChangeSeatsOnControlledVehicle);
     DEFINE_OPCODE_HANDLER(CMSG_CHANNEL_ANNOUNCEMENTS,                            STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleChannelAnnouncements      );
@@ -337,13 +337,13 @@ void OpcodeTable::Initialize()
     DEFINE_OPCODE_HANDLER(CMSG_LOGOUT_CANCEL,                                    STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleLogoutCancelOpcode        );
     DEFINE_OPCODE_HANDLER(CMSG_LOGOUT_REQUEST,                                   STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleLogoutRequestOpcode       );
     DEFINE_OPCODE_HANDLER(CMSG_LOG_DISCONNECT,                                   STATUS_NEVER,     PROCESS_INPLACE,      &WorldSession::Handle_EarlyProccess            );
-    DEFINE_OPCODE_HANDLER(CMSG_LOOT,                                             STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleLootOpcode                );
-    DEFINE_OPCODE_HANDLER(CMSG_LOOT_CURRENCY,                                    STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleLootCurrencyOpcode        );
-    DEFINE_OPCODE_HANDLER(CMSG_LOOT_MASTER_GIVE,                                 STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleLootMasterGiveOpcode      );
-    DEFINE_OPCODE_HANDLER(CMSG_LOOT_METHOD,                                      STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleLootMethodOpcode          );
-    DEFINE_OPCODE_HANDLER(CMSG_LOOT_MONEY,                                       STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleLootMoneyOpcode           );
-    DEFINE_OPCODE_HANDLER(CMSG_LOOT_RELEASE,                                     STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleLootReleaseOpcode         );
-    DEFINE_OPCODE_HANDLER(CMSG_LOOT_ROLL,                                        STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleLootRoll                  );
+    DEFINE_OPCODE_HANDLER(CMSG_LOOT,                                             STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleBatchedAction             );
+    DEFINE_OPCODE_HANDLER(CMSG_LOOT_CURRENCY,                                    STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleBatchedAction             );
+    DEFINE_OPCODE_HANDLER(CMSG_LOOT_MASTER_GIVE,                                 STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleBatchedAction             );
+    DEFINE_OPCODE_HANDLER(CMSG_LOOT_METHOD,                                      STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleBatchedAction             );
+    DEFINE_OPCODE_HANDLER(CMSG_LOOT_MONEY,                                       STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleBatchedAction             );
+    DEFINE_OPCODE_HANDLER(CMSG_LOOT_RELEASE,                                     STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleBatchedAction             );
+    DEFINE_OPCODE_HANDLER(CMSG_LOOT_ROLL,                                        STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleBatchedAction             );
     DEFINE_OPCODE_HANDLER(CMSG_MAIL_CREATE_TEXT_ITEM,                            STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleMailCreateTextItem        );
     DEFINE_OPCODE_HANDLER(CMSG_MAIL_DELETE,                                      STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleMailDelete                );
     DEFINE_OPCODE_HANDLER(CMSG_MAIL_MARK_AS_READ,                                STATUS_LOGGEDIN,  PROCESS_THREADUNSAFE, &WorldSession::HandleMailMarkAsRead            );
diff --git a/src/server/game/Server/WorldSession.h b/src/server/game/Server/WorldSession.h
index 1f775bc56d..262e137a40 100644
--- a/src/server/game/Server/WorldSession.h
+++ b/src/server/game/Server/WorldSession.h
@@ -1067,6 +1067,7 @@ class TC_GAME_API WorldSession
         void HandleViolenceLevel(WorldPacket& recvPacket);
         void HandleObjectUpdateFailedOpcode(WorldPacket& recvPacket);
         void HandleRequestCategoryCooldowns(WorldPacket& recvPacket);
+        void HandleBatchedAction(WorldPacket& recvPacket);
         void SendStreamingMovie();
         void HandleRequestResearchHistory(WorldPacket& recv_data);
int32 HandleEnableNagleAlgorithm();
10 Likes

Oh private servers that made classic wow fit on the 7.3.5 client? I’m sure this code will 100% work no problems

5 Likes

Sooo…. let me get this straight. Raiders get the 16 debuff limit built back in because it was a hardware limitation of the day, but putting it back in is authentic to vanilla, but the way the game processes CC in pvp has to remain current with modern WoW why exactly?

If they’re going to make Classic an authentic experience and have already said they’re building back in hardware limitations, do you really think they won’t put back ability batching?

25 Likes

The debuff limit was an intentional decision however bad it might have been. If they’d wanted it lifted completely they would have just lifted it when they raised the limit from 8 to 16.

And as for batching, it’s not just the way the game handled CC it’s the way the game handled everything.

2 Likes

I would love for this to be back in the game, nothing felt better than vanishing a missle-type spell, and it was fun to do.

24 Likes

Okay well, you’re aware that batching was an intentional design, too, right? They designed it that way because of limitations but kept it because it became a part of the game’s feel without any real downsides. They kept it all the way through MoP for crying out loud.

And yeah, that’s kind of my point, without batching the game’s dramatically different. When they removed it in WoD they had to make a bunch of posts about it because it felt noticeably different.

15 Likes

Yes, it was an intentional design based on hardware limitations. They didn’t want the limitation in the 1st place, but had no choice because of hardware limitations. As the hardware got better the limitation went up. From 8 to 16 to 40 to 256 to wherever it is now.

3 Likes

Regardless it was a design limitation not a physical limitation of the hardware.

1 Like