Playing D2R on the OneXPlayer 2 Pro with Arch Linux

D2R has already been out for four years. I used to play it on the Nintendo Switch, but recently I noticed a discount on the PC version, so I bought a copy for PC. Thanks to Wine, Valve, and the Steam Deck, playing games on Linux has become much easier. There are many articles about how to play D2R on a Steam Deck. However, since I’m using KWin with Wayland, I had a few issues to overcome.

Battle.net

Unlike Diablo IV, we can't run D2R directly through Steam. We need to launch Battle.net first. There are two issues here.

  1. we can't add non-steam game in Big-Picture mode.
  2. `Battle.net` has recently updated its server certificate, which is now signed by a new root certificate. To communicate with the server properly, we should use Proton 10 or later. Otherwise, running `Battle.net` will show BLZBNTBNA00000005 error.

There are two important points to note:

  1. If the Battle.net installer is not run with Proton 10 or later, switching to Proton 10 afterward will not resolve the issue — a complete installation is required.
  2. Before installing D2R, you should change the installation path in the Battle.net settings to a location outside the Steam folder. So that we don't need to install D2R again if we remove and install `Battle.net` again.

Windowed of Battle.net

Battle.net may be run in windowed mode, a titlebar and frame is added by KWin. It will make the Battle.net running in a wrong resolution, and the play button won't be shown. We can add a KWin Window Rule to force No titlebar and frame and the minimum size of the window.

Controller

D2R works out of the box with mouse and keyboard. But if you want to play it with a controller, you should enable Steam Input. After the press any button screen is passed, D2R can be controlled by a controller. We can touch the screen, switch the controller to mouse mode, or map a button of the controller to send a input event using keyd(There is no extra key in OneXPlayer 2 Pro controller. The keyboard mode switch key is hardware controlled, and the system can't read that key event.) to pass the press any button screen. It looks like D2R will show press A button screen instead of press any button screen if we don't attach any mouse. We can use a controller to pass that screen.

Enable Steam Input

Steam Input uses uinput to handle the event from controllers. By default, the user running steam doesn't have enough permission. We need to install game-devices-udev` to change the permission of the device.

In Arch Linux, there is an AUR package, install it by running: paru -S game-devices-udev, change paru to the aur helper you are using.

Cursor in Inventory UI with Controller

In the inventory ui, the cursor might be not moved correctly. The position of the cursor is moved, but the cursor surface is not. We can run D2R with gamescope to solve this problem.

  1. Install gamescope. Because my steam is installed by flatpak, so I should install gamescope by flatpak too: flatpak --user install org.freedesktop.Platform.VulkanLayer.gamescope, choose the same version of org.freedesktop.Platform needed by steam.
  2. Add the bin folder of gamescope to PATH of steam in flatpak settings, the bin folder is /usr/lib/extensions/vulkan/gamescope/bin.
  3. Set the launch option
# Ghost editor replace -- with –
gamescope --hide-cursor-delay -1 -- %command%

The cursor surface might not be shown at first, we can move the mouse to make it appear.

Gamescope Error Popups

Launching D2R, there are two gamescope error popup windows: CreateSwapchainKHR and QueuePresentKHR. We can just click OK to pass them. Don't click Cancel, otherwise, D2R will display a black screen. When I disable WSI layer with ENABLE_GAMESCOPE_WSI=0, those popups are gone. The launch option becomes: ENABLE_GAMESCOPE_WSI=0 gamescope --hide-cursor-delay -1 – %command%. When WSI layer is disabled, the cursor in inventory ui becomes laggy.

Unsolved

Steam Freeze

This is an issue of steam. If steam lost-focus(may) or the screen is locked(must), steam will freeze. the game might lose control or be frozen. I'm not sure whether disabling Steam Input will prevent Steam freeze from affecting the game. I'm also not looking into the lost-focus issue — it seems to be related to the KWin compositor's behavior during the game, such as when pressing Alt+Tab.

What a heartbeat moment — playing D2R Hardcore mode and experiencing an unexpected freeze!

Running D2R without Opening Steam

I think I can run D2R directly to avoid the steam freeze problem. I used pstree -plas $PID to find out the parent process running Battle.net, and used cat /proc/$PID/environ | tr '\0' '\n' to print all its environment variables. I kept almost all variables starting with STEAM. After that, I can run D2R with Proton directly.

Controller Mapping

I found that the controller works out of box opening outside of steam. It looks like steam disables my controller using a SDL environment variable. I am a Nintendo controller guy. Using Xbox controller layout is bit inconvenient for me. Thanks for SDL, I can use a Nintendo controller layout. Here is my controller mapping edited via AntiMicroX:

SDL_GAMECONTROLLERCONFIG="030081b85e0400008e020000080100001118654,Atari Xbox 360 Game Controller,platform:Linux,a:b1,b:b0,x:b3,y:b2,back:b6,start:b7,guide:b8,leftshoulder:b4,rightshoulder:b5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,"

Gamescope

To my surprise, if I run D2R with gamescope nothing will show but the Battle.net is actual running. Comparing the processes between steam-run and direct-run, I found the proton process executed by gamescope will create a child process and exit in direct-run. So, gamescope thinks there is no application running. I finally wrote a script to get the flatpak instance id with (--instance-id-fd) and monitor that instance id with (flatpak ps) to keep the process running.

Then comes the second surprise. Unlike steam-run, all windows (Battle.net launcher and D2R) are running inside a window. Actually, the direct-run behavior is what I would expect based on my understanding. It is what a nested compositor should do. But the scale of the cursor is wrong and I must manually set the resolution. I really want the steam-run behavior. Comparing the environment variables, I found Display is set to :0 in steam-run and Display is set to :1. So, all windows of steam-run is drawing directly by Kwin. But why does the cursor in the inventory ui affects?

During the search, I found that gamescope actually doesn't work with the builtin Proton. I should install com.valvesoftware.Steam.CompatibilityTool.Proton-GE. After all windows of steam-run will run inside the window of gamescope. And those error popups disappear. But the cursor in the inventory ui doesn't move. After adding `--force-grab-cursor`, the cursor moves but it twinkles.

Scripts

Here are my scripts:

  • run-steam-battlenet
#! /usr/bin/env bash

set_envs() {
    set -a
    STEAM_BASE_FOLDER=$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam
    STEAM_CLIENT_CONFIG_FILE=$HOME/.var/app/com.valvesoftware.Steam/.local/share/steam.cfg
    STEAM_COMPAT_APP_ID=0
    STEAM_COMPAT_CLIENT_INSTALL_PATH=$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam
    STEAM_COMPAT_DATA_PATH=$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/compatdata/$EX_STEAM_GAME_ID
    STEAM_COMPAT_LIBRARY_PATHS=$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps
    STEAM_COMPAT_MEDIA_PATH=$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/shadercache/$EX_STEAM_GAME_ID/fozmediav1
    STEAM_COMPAT_MOUNTS="$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/Proton - Experimental":$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/SteamLinuxRuntime_sniper
    STEAM_COMPAT_PROTON=1
    STEAM_COMPAT_TOOL_PATHS="$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/Proton - Experimental":$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/SteamLinuxRuntime_sniper
    STEAM_COMPAT_TRANSCODED_MEDIA_PATH=$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/shadercache/$EX_STEAM_GAME_ID
    STEAM_EXTRA_COMPAT_TOOLS_PATHS=/app/share/steam/compatibilitytools.d:/app/utils/share/steam/compatibilitytools.d
    STEAM_FOSSILIZE_DUMP_PATH=$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/shadercache/$EX_STEAM_GAME_ID/fozpipelinesv6/steamapprun_pipeline_cache
    STEAM_RUNTIME_LIBRARY_PATH=$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_32/steam-runtime/pinned_libs_32:$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_32/steam-runtime/pinned_libs_64:/app/lib/i386-linux-gnu/GL/default/lib:/app/lib32:/app/lib/i386-linux-gnu:/lib64:/app/lib:/usr/lib/x86_64-linux-gnu/GL/default/lib:/usr/lib/x86_64-linux-gnu/openh264/extra:/usr/lib/x86_64-linux-gnu:$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_32/steam-runtime/lib/i386-linux-gnu:$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_32/steam-runtime/usr/lib/i386-linux-gnu:$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_32/steam-runtime/lib/x86_64-linux-gnu:$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_32/steam-runtime/usr/lib/x86_64-linux-gnu:$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_32/steam-runtime/lib:$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_32/steam-runtime/usr/lib
    STEAM_ZENITY=/usr/bin/zenity
    DISPLAY=${DISPLAY_BEFORE_GAMESCOPE:-$DISPLAY}
    set +a
}

set_controller_mapping() {
    set -a
    SDL_GAMECONTROLLERCONFIG="030081b85e0400008e020000080100001118654,Atari Xbox 360 Game Controller,platform:Linux,a:b1,b:b0,x:b3,y:b2,back:b6,start:b7,guide:b8,leftshoulder:b4,rightshoulder:b5,leftstick:b9,rightstick:b10,leftx:a0,lefty:a1,rightx:a3,righty:a4,lefttrigger:a2,righttrigger:a5,dpup:h0.1,dpleft:h0.8,dpdown:h0.4,dpright:h0.2,"
    set +a
}

start_bottle_net() {
    # Create a temp file for storing the instance id
    tmpfile_instance_id=$(mktemp /tmp/run-steam-battlenet-instance-id.XXXXXX)
    echo "Steam instance id will be stored at $tmpfile_instance_id"

    exec 3>"$tmpfile_instance_id"

    /usr/bin/flatpak run --instance-id-fd=3 --branch=stable --arch=x86_64 --command="$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/common/Proton - Experimental/proton" com.valvesoftware.Steam waitforexitandrun "$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/compatdata/$EX_STEAM_GAME_ID/pfx/drive_c/Program Files (x86)/Battle.net/Battle.net Launcher.exe"

    exec 3>&-
    set +e

    read instance_id < "$tmpfile_instance_id"
    echo "Running as Battle.net as Steam Instance[$instance_id]"

    while flatpak ps --columns=instance | grep -qx "$instance_id"; do
        sleep 1
    done
}

delete_file() {
    if [ -n "$1" ]
    then
        if [ -f "$1" ]
        then
            rm "$1"
        fi
    fi
}

clean_up() {
    delete_file "$tmpfile_instance_id"
}

EX_STEAM_GAME_ID=3494142050

set -e
tmpfile_instance_id=""
trap 'clean_up' EXIT

set_envs
set_controller_mapping
start_bottle_net
  • run-steam-battlenet-with-gamescope
#! /usr/bin/env bash

export DISPLAY_BEFORE_GAMESCOPE="$DISPLAY"

/usr/bin/gamescope --hide-cursor-delay=-1 -- "$(dirname $0)/run-steam-battlenet"