It sounds like you are using some kind of callback system to handle incoming messages. This is entirely reasonable but it does not obviate the need for a game loop. In fact, "one big timer" idea is basically exactly the same as a game loop. A typical client might have a loop like:
In a client, you want to run as fast as possible. If V-sync is enabled, then
flipBuffers() will block and prevent the loop from running too quickly. That is why we don't use timers typically for game loops, because we need it to start over immediately to write the next frame. On a server, this might look like:
time start = now();
waitUntilTime(start + 100ms);
There is much less work to do, because we don't need to render the game world. But we still need to run the game logic, create and destroy entities, damage players, handle logins, etc. The architecture you describe as using seems to be based on reaction to network messages. When a player requests to attack, they kick off a timer that repeatedly attacks. This is putting the state of a player (i.e. whether they are "continuously attacking") into the timer manager. While it might seem simple to code, this is likely not a good idea because it requires extra code to handle cancelling the timer, etc. It is cleanest to store this state as explicitly as possible, as opposed to implicitly in the presence or absence of a timer.
You probably have a
Player object on the server for each connected player. They would have a boolean field
isAttacking, and a time field
lastAttack. Then during object update, if
lastAttack + 1sec < stepStart + stepDuration, we say the player can attack that step. Then crucially we update
lastAttack = laterOf(lastAttack + 1sec, stepStart). Why not just say
lastAttack = stepStart? Because we want the player to attack once a second, and otherwise depending on how our steps actually line up with the 1 second interval, we might be off by 100ms, which wouldn't be fair to multiple players. On a client this usually is ignored because a frame is 16 or 30ms, so the error there is negligible.
Now the response to the network message is just to set or clear the
isAttacking flag in the player. Thus the architecture is driven by the game loop, rather than in response to messages. The
laterOf function ensures that we don't need to worry about the
lastAttack time being in the far past if the player stops attacking, then later starts up again. If we didn't do this, they would attack every frame until we had added enough 1sec intervals to catch up!
Note that timers provided by the language or OS are probably expensive. A small constant number is fine, but one for every player will not scale to many users.