Skip to content

Local coop#8354

Open
yuripourre wants to merge 8 commits intodiasurgical:masterfrom
yuripourre:local-coop
Open

Local coop#8354
yuripourre wants to merge 8 commits intodiasurgical:masterfrom
yuripourre:local-coop

Conversation

@yuripourre
Copy link
Copy Markdown
Collaborator

@yuripourre yuripourre commented Dec 13, 2025

This is my proposal for local coop. I wanted this feature since I played Diablo for the first time via modem and the game kept disconnecting when someone called.

I am creating this PR so the implementation can be discussed and people can try it. I am really open to suggestions, opinions and even abandon this PR if it's not the direction the community wants to follow. Most of the code is AI generated so I don't want to overwhelm anyone asking for code review yet since I am planning to refactor this entire branch.

I tried to make it completely optional and even if enabled should not be intrusive in any way. The UI is close as possible to the original design, nothing fancy, nothing modern.

Limitations

  • SDL cannot guarantee that the gamepads will be assigned to the same players when the game restarts
  • There is no way to control the cursor as a mouse so spells that requires cursor position will not work as expected
  • No color distinctions (waiting for: [QOL/UX] Multiplayer Player Colors #8317)
  • Camera movement is jittery when all players move in the same direction.
WARNING: This is a very experimental feature, backups your characters before using them in local coop!

How to activate it

First of all, you should enable the setting "Enable local co-op".

Once in a multiplayer game, the main UI panel is hidden and the local co-op panel is displayed, if the game detects more than 1 gamepad, other players will display the character selection panel. The list of characters is based on the multiplayer saved characters.

Gameplay

Panels

The character panel and inventory are fully functional. I introduced the concept of ownership, whoever opens the panel has control over the panel and can do the same actions as a single player.

NPCs Interactions

Work similarly to other panels (whoever opens gets ownership).

Spell Assignment

Alternatively to the default way to assign spells, it's also possible to assign spells to skill slots, so you can have up to 4 spells assigned to your character.

To assign a spell, you have to long press one of the buttons A,B,X or Y. It will open a speedbook and once you press one of those buttons again, the spell/skill is assigned.

Spells can be casted using the assigned button.

Belt

I decided to keep the full belt for players. I wanted the local coop panel to be optional for a single player mode in the future.
To use a belt item you must press one of the shoulder buttons.

Left shoulder button highlights items 1 to 4, right shoulder button highlights items 5 to 8. Once the item is highlighted if you press A,B,X,Y the item will be consumed.

Screenshot From 2025-12-12 20-08-34 Screenshot From 2025-12-26 15-38-54
local_coop_town_trim.mp4
local_coop_dungeon_trim.mp4

Closes #1237

@yuripourre yuripourre marked this pull request as draft December 13, 2025 04:46
@yuripourre
Copy link
Copy Markdown
Collaborator Author

Turning into draft until I can rebase it.

@glebm
Copy link
Copy Markdown
Collaborator

glebm commented Dec 13, 2025

I think some of the files in this PR, such as control.cpp, had their line endings changed from CRLF to LF which makes the diff huge

@glebm
Copy link
Copy Markdown
Collaborator

glebm commented Dec 13, 2025

To assign a spell, you have to long press one of the buttons A,B,X or Y. It will open a speedbook and once you press one of those buttons again, the spell/skill is assigned.

I guess this changes the controller scheme a bit because I thought we already had spell assignment with SELECT + A/B/X/Y on the speedbook?

@yuripourre
Copy link
Copy Markdown
Collaborator Author

To assign a spell, you have to long press one of the buttons A,B,X or Y. It will open a speedbook and once you press one of those buttons again, the spell/skill is assigned.

I guess this changes the controller scheme a bit because I thought we already had spell assignment with SELECT + A/B/X/Y on the speedbook?

Hmmm, yeah, I changed the controller scheme so the actions have less steps so less interactions to handle and the game is more dynamic (in my biased opinion). In this PR, select opens the character panel and start opens the inventory.

I can make it more similar to the current controller scheme.

@glebm
Copy link
Copy Markdown
Collaborator

glebm commented Dec 14, 2025

I can make it more similar to the current controller scheme.

Let's gather some more opinions, I haven't even tried this branch yet, maybe it is a lot better.

@yuripourre
Copy link
Copy Markdown
Collaborator Author

yuripourre commented Dec 24, 2025

This PR is not perfect:

  • Sometimes the players get stuck and cannot move
  • The UI is not polished
  • I haven't test interactions between players (only monsters)
  • I probably broke something during rebase

But I think it's in a good shape to start discussions.

@yuripourre yuripourre marked this pull request as ready for review December 24, 2025 05:48
@yuripourre yuripourre force-pushed the local-coop branch 6 times, most recently from 1fdc1b6 to 15740b3 Compare December 25, 2025 21:06
@yuripourre
Copy link
Copy Markdown
Collaborator Author

Ready for review

@yuripourre yuripourre requested a review from glebm December 27, 2025 01:48
@HoofedEar
Copy link
Copy Markdown
Contributor

Incredible work!

My only complaint/issue I've found is when both players are moving in the same direction at the same time, the screen gets a bit jittery? This is at 480p, windowed mode, on Windows 11

devilutionx_xlVbKgCLWJ.mp4

@yuripourre
Copy link
Copy Markdown
Collaborator Author

Incredible work!

My only complaint/issue I've found is when both players are moving in the same direction at the same time, the screen gets a bit jittery? This is at 480p, windowed mode, on Windows 11
devilutionx_xlVbKgCLWJ.mp4

Whoa! Thank you do much for testing. I am glad you only had ONE complain, that shows me I could cover a lot of edge-cases.

I tried to fix the camera, that was the best I could get last week. I can try another approach, I will let you know when I have something better.

Btw, if you want to try 3 or 4 players I highly recommend changing the screen resolution to 720p. I couldn't try 4 players yet because one of my gamepads is not working, I have to find the other one.

@strich
Copy link
Copy Markdown
Collaborator

strich commented Jan 28, 2026

Mate this is actually extremely cool! Hope it gets reviewed soon as it looks like a lot of work to then get stale.

@strich strich added the enhancement New feature or request label Jan 28, 2026
@yuripourre
Copy link
Copy Markdown
Collaborator Author

@strich Thanks! You just reminded me that I need to work on the conflicts.

Yeah, I really enjoyed working on this feature. I wanted to do that for a loooong time.

@yuripourre
Copy link
Copy Markdown
Collaborator Author

Oh damn! I broke the gamepad logic during the rebase. 😅

@yuripourre yuripourre force-pushed the local-coop branch 7 times, most recently from 24be989 to 6bf198a Compare February 1, 2026 23:28
@yuripourre yuripourre force-pushed the local-coop branch 2 times, most recently from bef20a7 to c1fd3f0 Compare April 13, 2026 19:49
Introduces the local co-op subsystem as a self-contained module under
Source/controls/local_coop/:

- local_coop_constants.hpp  — layout, camera, and input constants
- local_coop_assets.hpp/.cpp — asset loading/unloading for coop HUD
- local_coop_button_mapper.hpp/.cpp — per-player gamepad button routing
- local_coop.hpp/.cpp — core state machine: player join/leave, camera,
  panel layout, input dispatch, hero selection, and visual store wiring

Source/CMakeLists.txt is updated to include the new source files in the
build.
- dvlnet/loopback: message queue now stores sender ID alongside the
  message payload so local co-op players can exchange messages through
  the loopback provider without being collapsed onto player 0
- multi.cpp / init.cpp: call LoadAvailableHeroesForAllLocalCoopPlayers()
  after the save file is read so co-op slots know which heroes are
  eligible
- options.cpp/.h: expose IsLocalCoopEnabled() option
- player.cpp/.h: track whether a Player slot belongs to a local co-op
  player; add helpers queried by other subsystems
- pfile.cpp/.h: enumerate hero save files for co-op slot selection;
  save/restore all local co-op player heroes on game exit
- loadsave.cpp: load and save extra player slots during level transitions
- portal.cpp: create portals for local co-op players on level change
- lighting.cpp/.h: expose per-player light source so co-op players each
  carry their own light radius independently of player 1
- msg.cpp: route network messages to the correct local co-op player slot
- controller.cpp/controller_motion.cpp: detect hot-plug of additional
  gamepads and assign them to co-op player slots
- devices/game_controller.h, joystick.cpp/.h: expose device index so
  each physical controller maps to a specific co-op player
- game_controls.cpp: dispatch button events to the owning co-op player
  rather than always targeting player 1
- modifier_hints.cpp/.h: show per-player modifier hint overlays
- plrctrls.cpp/.h: route padmenu navigation, attack, skill, belt, and
  inventory actions to the correct local co-op player; extract
  DpadGamepadButtonToAxisDirection helper; use skillSlotSize constant
- touch/event_handlers.cpp: guard touch events against co-op mode
- scrollrt.cpp: split the viewport so each local co-op player sees their
  own screen region; expand the walking tile buffer when any co-op player
  is in motion; apply per-player camera offset; render item/monster/object
  highlights for all co-op players; skip lighting attenuation for local
  players who carry their own light source
- light_render.cpp: apply lighting correctly per player when multiple
  local players are present
- text_render.cpp: clip text to the active player's panel region
- automap.cpp: center the automap on the correct player's position in
  split-screen mode
- control.hpp: add ShouldHideMainPanelForLocalCoop() and panel-ownership
  helpers so each co-op player's HUD occupies its own screen quadrant
- control_infobox.cpp: render the info box inside the owning player's
  panel region
- control_panel.cpp: lay out life/mana globes, belt, and skill slots per
  player; draw experience bar; render attribute-available icon
- stores.cpp: gate store access to the interacting player; wire
  InitVisualStore / CloseVisualStore / FreeVisualStoreGFX into the
  town-load and store-close paths via CloseLocalCoopStore
- spells.h: expose speed-spell slot count constant (skillSlotSize)
- cursor.cpp: clamp cursor to the active player's panel region in co-op
- diablo.cpp: call CloseVisualStore from QuestLogKeyPressed and
  DisplaySpellsKeyPressed; initialise and free the visual store on
  town load/unload; route key events to the correct co-op player
- inv.cpp: route inventory open/close, item pick-up, and equipment
  actions to the player whose panel was interacted with
- items.cpp/.h: allow any local co-op player to pick up and use items;
  guard item-use actions by player proximity
- levels/trigs.cpp: trigger level-change portals for all active local
  co-op players when any player steps on a trigger
- missiles.cpp: spawn healing/mana missiles for all local co-op players
  when appropriate (e.g. town potions)
- objects.cpp: allow co-op players to interact with objects inside their
  proximity range
- qol/autopickup.cpp: run auto-pickup logic for each local co-op player
- towners.cpp: allow co-op players to talk to town NPCs
- lua/lua_event.hpp: declare Lua event hooks needed by the co-op module
- utils/hp_mana_units.hpp: extend HP/mana unit helpers used by co-op HUD
- reencode_dun_cels.cpp: initialise frameSize to 0 to silence
  -Wmaybe-uninitialized on paths where GetReencodedSize returns early
- spell_list.cpp: initialise spells to 0 to silence
  -Wmaybe-uninitialized in IsValidSpeedSpell
- platform/ctr/keyboard.cpp: change eventCount to size_t and introduce
  VkbdEventQueueCapacity constant to silence -Wsign-compare against
  sizeof(events)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Couch co-op multiplayer

4 participants