Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
04ba0f0
README: say the mod is Windows only and why the Android LeviLauncher …
claude Nov 11, 2026
be673ec
Android: first experimental version for LeviLauncher on Android
claude Nov 11, 2026
780ee9f
Android CI: use the runner's Android SDK instead of android-actions/s…
claude Nov 11, 2026
c5972a7
Android 0.1.1: search type names in the game's executable segment too
claude Nov 11, 2026
76ef7fa
Android 0.1.2: turn split screen on (AppPlatform::allowSplitScreen, s…
claude Nov 11, 2026
7ebe41e
Android 0.1.3: write the Download log as it happens, count allowSplit…
claude Nov 11, 2026
60811d7
Android 0.1.4: log the game's JNI functions for input
claude Nov 11, 2026
f710b26
Android CI: publish the newest build as the android-latest pre-releas…
claude Nov 12, 2026
77b5570
Android 0.1.5: log which device each controller event comes from, and…
claude Nov 12, 2026
5574e59
Android 0.1.6: log the game's input imports and controller classes
claude Nov 12, 2026
ce389ad
Android 0.1.7: log the device of each gamepad key via GameActivityKey…
claude Nov 12, 2026
51b9dd3
Android 0.1.8: find and watch the game's native controller manager
claude Nov 12, 2026
f98cbce
Android 0.1.9: find the gamepad system and log who calls it
claude Nov 12, 2026
de2b6ac
Android 0.1.10: dump the code along the input call chain
claude Nov 12, 2026
907534e
Android 0.2.0: one player per controller (first attempt)
claude Nov 12, 2026
62284a7
Android 0.2.1: let other controllers join (MinecraftGame splitscreen …
claude Nov 12, 2026
1824cdb
Android 0.2.2: Start on another controller adds a player
claude Nov 12, 2026
7b49700
Android 0.2.3: watch the user connection behind adding a player
claude Nov 12, 2026
97a0ae0
Android 0.2.4: add the player without waiting for a user sign-in
claude Nov 12, 2026
1c01b8f
Android 0.2.5: trace MinecraftGame after adding a player
claude Nov 12, 2026
9a5a722
Android 0.2.6: dump the check that refuses the new player
claude Nov 12, 2026
4fa7fa8
Android 0.2.7: let the add-player check pass for the new player
claude Nov 12, 2026
c4ea2b5
Android 0.2.8: trace the user manager too while adding a player
claude Nov 12, 2026
8493b72
Android 0.2.9: log what the user manager answers for the new player
claude Nov 12, 2026
0da68f7
Android 0.3.0: lend player 1's user to the new player
claude Nov 12, 2026
840ec3d
Android 0.3.1: the second player can join again
claude Nov 12, 2026
da5f3ad
Android 0.3.2: Start on a controller that already plays no longer add…
claude Nov 12, 2026
1bfd910
Android 0.4.0: player 1 keeps touch, every controller joins as a new …
claude Nov 12, 2026
90d6828
Android 0.5.0: groundwork for in-game menus (forms)
claude Nov 12, 2026
cd4ed4d
Android 0.5.1: send a first test form to an added player
claude Nov 12, 2026
b5a77ec
Android 0.5.2: a test form each time a player joins, again while it i…
claude Nov 13, 2026
74b02e7
Android 0.6.0: Split screen menu on Select (leave split screen)
claude Nov 13, 2026
636b11b
Android 0.6.1: leave split screen after the form screen closes; forge…
claude Nov 13, 2026
bdd8c9c
Android 0.6.2: leave split screen through ClientInstance::requestLeav…
claude Nov 13, 2026
6aba523
Android 0.6.3: leave split screen by setting the client's leave flag;…
claude Nov 13, 2026
93c51ac
Android 0.6.4: leave split screen with the leave flags the pause menu…
claude Nov 13, 2026
2d48a4f
Android 0.6.5: one Split screen menu at a time per player
claude Nov 13, 2026
381e5cf
Android 0.6.5: an unanswered menu stops blocking Select after a minute
claude Nov 13, 2026
2f338d5
Android 0.7.0: swap places, become a new player, who-is-playing picke…
claude Nov 13, 2026
818f712
Android 0.7.1: notice switched-off controllers (Java InputDevice, els…
claude Nov 13, 2026
24fcbb9
Android 0.7.2: the SplitScreenUI resource pack as an .mcpack; black C…
claude Nov 13, 2026
1d5b7bb
Android 0.7.3: the resource pack comes inside the mod and is installe…
claude Nov 13, 2026
c0f42d1
Android 0.8.0: the menus and joining follow the Windows version's log…
claude Nov 13, 2026
bbc3d60
Android 0.8.1: no menu on a player who is leaving
claude Nov 13, 2026
df9c512
Android 0.8.2: a leaving player gets no input and no new controller (…
claude Nov 13, 2026
c01838b
Android 0.9.0: players join through the game's own prompt (startJoins…
claude Nov 13, 2026
c3d4979
Android 0.9.1: Start adds a player in a loaded world (the game shows …
claude Nov 13, 2026
50df6ad
Android 0.9.2: taking a player whose own player must leave moves the …
claude Nov 13, 2026
c9b1b32
Android 0.9.3: the mod's delayed work also runs while the only contro…
claude Nov 13, 2026
e2bd798
Android 0.9.4: a controller changes player only when it holds no butt…
claude Nov 13, 2026
0961c85
Android 0.9.5: a player leaves once its last menu is 6 s old; dump th…
claude Nov 13, 2026
cf58fe2
Android 0.9.6: find and trace the game's own leave function (the Clie…
claude Nov 13, 2026
1ef3301
Android 0.9.7: trace ClientInstance's functions (instead of Minecraft…
claude Nov 13, 2026
46a52bc
Android 0.9.8: players leave through the game's own requestLeaveGame …
claude Nov 13, 2026
aac5b52
Android 0.9.9: log the pause menu controller's calls with what their …
claude Nov 13, 2026
e82375b
Android 0.9.10: find the pause menu's button names and the code that …
claude Nov 14, 2026
842fffe
Android 0.9.11: log the pause menu's screen events (all but pointer m…
claude Nov 14, 2026
581a518
Android 0.10.0: the pause menu's Split screen button opens the Split …
claude Nov 14, 2026
336cf53
Android 0.10.1: read the pause menu button's hash and state at their …
claude Nov 14, 2026
9df9e8b
Android 0.10.2: the pause menu's Split screen button pressed by touch…
claude Nov 14, 2026
38e3bff
Android 0.10.3: the pause menu's Split screen button closes the pause…
claude Nov 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Builds the Android version (android/) into SplitScreenMod-android-arm64.zip / .levipack for LeviLauncher on Android.
name: Android

on:
pull_request:
push:
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write # to publish the android-latest pre-release
steps:
- uses: actions/checkout@v7

# The runner already has the Android SDK; android-actions/setup-android failed on its own sdkmanager install.
# NDK 28.2 is installed if missing; the runner's newest NDK is the fallback.
- run: |
sudo apt-get update
sudo apt-get install -y ninja-build
NDK="$ANDROID_HOME/ndk/28.2.13676358"
if [ ! -d "$NDK" ]; then
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" "ndk;28.2.13676358" > /dev/null || true
fi
if [ ! -d "$NDK" ]; then NDK="$ANDROID_NDK_LATEST_HOME"; fi
echo "Using NDK $NDK"
echo "NDK=$NDK" >> "$GITHUB_ENV"

- run: |
cmake -S android -B build-android -G Ninja \
-DCMAKE_TOOLCHAIN_FILE="$NDK/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-28 -DANDROID_STL=c++_shared \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-android --target levi_package

# The mod's resource pack (menu look, black "Choose your player" screen, texts in every language), imported
# into Minecraft once. Its pause menu button is for Windows only and is left out.
- run: |
cd resource_packs/SplitScreenUI
zip -qr ../../build-android/SplitScreenUI.mcpack . -x 'ui/pause_screen.json'
mkdir -p ../../build-android/package && cp ../../build-android/SplitScreenUI.mcpack ../../build-android/package/

- uses: actions/upload-artifact@v7
with:
name: ${{ github.event.repository.name }}-${{ github.sha }}-android-arm64
path: build-android/package/

# The newest Android build is always at the same link, so it can be downloaded straight on the phone:
# https://github.com/<owner>/<repo>/releases/download/android-latest/SplitScreenMod-android-arm64.zip
- if: github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release delete android-latest --cleanup-tag -y || true
gh release create android-latest build-android/SplitScreenMod-android-arm64.zip build-android/SplitScreenUI.mcpack \
--prerelease --target "$GITHUB_SHA" --title "Android (experimental, newest build)" \
--notes "Newest experimental Android build from ${GITHUB_REF_NAME} (${GITHUB_SHA::7}). Add it in LeviLauncher for Android with Mods > Add Mod, and import SplitScreenUI.mcpack into Minecraft (Settings > Global Resources) for the menus."
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Minecraft Bedrock already contains split-screen (it is used on Xbox), but the Wi

## Installation

> **Android:** the Windows zip does not work on Android (LeviLauncher for Android shows "Invalid ZIP Mod Package", because it needs an Android `.so`, not a Windows `.dll`). An experimental Android version is in [`android/`](android/README.md).

**Easiest:** in [LeviLauncher](https://github.com/LiteLDev/LeviLauncher), open your instance's **Manage Mods**, press **Import .mcmod** and pick `SplitScreenMod.mcmod` from the [latest release](../../releases/latest).

**By hand:**
Expand Down
90 changes: 90 additions & 0 deletions android/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# SplitScreenMod for LeviLauncher on Android (preloader-android SDK).
# Build: see android/README.md. The result is a .levipack (a zip) that LeviLauncher for Android imports.
cmake_minimum_required(VERSION 3.22)

project(SplitScreenModAndroid LANGUAGES CXX ASM)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

set(MOD_ID "SplitScreenMod" CACHE STRING "LeviLauncher mod package directory name")
set(MOD_NAME "SplitScreenMod" CACHE STRING "LeviLauncher mod display name")
set(MOD_AUTHOR "SnakyWarrior" CACHE STRING "LeviLauncher mod author")
set(MOD_VERSION "0.10.3" CACHE STRING "LeviLauncher mod version")
set(MOD_LIBRARY_NAME "SplitScreenMod" CACHE STRING "Native library name without lib prefix or .so suffix")
set(MOD_MINECRAFT_VERSIONS "[\"1.26.*\"]" CACHE STRING "Raw JSON array for manifest minecraft_versions")

if(NOT ANDROID)
message(FATAL_ERROR "Configure with the Android NDK toolchain file (see android/README.md)")
endif()
if(NOT CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a")
message(FATAL_ERROR "Only arm64-v8a is supported, got ${CMAKE_ANDROID_ARCH_ABI}")
endif()

include(FetchContent)
FetchContent_Declare(
preloader_android
GIT_REPOSITORY https://github.com/LiteLDev/preloader-android.git
GIT_TAG 0.2.2
SOURCE_SUBDIR cmake/preloader-sdk
)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 11.2.0
)
set(FMT_DOC OFF CACHE BOOL "" FORCE)
set(FMT_TEST OFF CACHE BOOL "" FORCE)
set(FMT_INSTALL OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(preloader_android fmt)
FetchContent_GetProperties(preloader_android)

# The SDK symbols are provided by the preloader that LeviLauncher has already loaded.
option(LEVI_LINK_PRELOADER "Build and link the preloader target instead of resolving SDK symbols at runtime" ON)
if(LEVI_LINK_PRELOADER AND NOT TARGET preloader)
add_subdirectory("${preloader_android_SOURCE_DIR}" "${CMAKE_BINARY_DIR}/_preloader_android")
endif()

add_library(${MOD_LIBRARY_NAME} SHARED src/main.cpp src/SplitScreen.cpp src/Elf.cpp src/InputProbe.cpp src/DeviceManagerProbe.cpp src/ControllerSplit.cpp src/FormProbe.cpp src/Menus.cpp src/VTrace.cpp src/PostTrace.cpp src/Trampolines.S src/VTrampolines.S src/PostTrampolines.S)
target_include_directories(${MOD_LIBRARY_NAME} PRIVATE src "${preloader_android_SOURCE_DIR}/include")
target_compile_definitions(${MOD_LIBRARY_NAME} PRIVATE SPLITSCREEN_VERSION="${MOD_VERSION}")
target_compile_options(${MOD_LIBRARY_NAME} PRIVATE -Wall -Wextra -fvisibility=hidden -fvisibility-inlines-hidden)
target_link_options(${MOD_LIBRARY_NAME} PRIVATE "-Wl,-z,max-page-size=16384")

find_library(log-lib log REQUIRED)
target_link_libraries(${MOD_LIBRARY_NAME} PRIVATE ${log-lib} android dl fmt::fmt)
if(TARGET preloader)
target_link_libraries(${MOD_LIBRARY_NAME} PRIVATE preloader)
else()
target_link_options(${MOD_LIBRARY_NAME} PRIVATE "-Wl,--unresolved-symbols=ignore-all")
endif()

add_custom_command(TARGET ${MOD_LIBRARY_NAME} POST_BUILD
COMMAND "${CMAKE_STRIP}" --strip-unneeded "$<TARGET_FILE:${MOD_LIBRARY_NAME}>"
VERBATIM
)

configure_file("${CMAKE_CURRENT_SOURCE_DIR}/manifest.json.in" "${CMAKE_CURRENT_BINARY_DIR}/manifest.json" @ONLY)

# SplitScreenMod-android-arm64.levipack: a zip with SplitScreenMod/manifest.json, SplitScreenMod/libSplitScreenMod.so,
# config/ and pack/ (the resource pack).
# The same zip is also written as .zip, which LeviLauncher's "Add Mod" accepts.
set(PACKAGE_ROOT "${CMAKE_CURRENT_BINARY_DIR}/package")
set(PACKAGE_MOD_DIR "${PACKAGE_ROOT}/${MOD_ID}")
set(PACKAGE_FILE "${CMAKE_CURRENT_BINARY_DIR}/${MOD_ID}-android-arm64.levipack")
add_custom_target(levi_package
COMMAND "${CMAKE_COMMAND}" -E rm -rf "${PACKAGE_ROOT}"
COMMAND "${CMAKE_COMMAND}" -E make_directory "${PACKAGE_MOD_DIR}"
COMMAND "${CMAKE_COMMAND}" -E copy "$<TARGET_FILE:${MOD_LIBRARY_NAME}>" "${PACKAGE_MOD_DIR}/lib${MOD_LIBRARY_NAME}.so"
COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_CURRENT_BINARY_DIR}/manifest.json" "${PACKAGE_MOD_DIR}/manifest.json"
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/config" "${PACKAGE_MOD_DIR}/config"
# the mod's resource pack, put into the game and turned on by the mod, with the Android changes from android/pack
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/../resource_packs/SplitScreenUI" "${PACKAGE_MOD_DIR}/pack"
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/pack" "${PACKAGE_MOD_DIR}/pack"
COMMAND "${CMAKE_COMMAND}" -E chdir "${PACKAGE_ROOT}" "${CMAKE_COMMAND}" -E tar cf "${PACKAGE_FILE}" --format=zip "${MOD_ID}"
COMMAND "${CMAKE_COMMAND}" -E copy "${PACKAGE_FILE}" "${CMAKE_CURRENT_BINARY_DIR}/${MOD_ID}-android-arm64.zip"
DEPENDS ${MOD_LIBRARY_NAME}
VERBATIM
)
40 changes: 40 additions & 0 deletions android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# SplitScreenMod for Android (experimental)

The Windows mod (in `src/`) cannot run on Android: it is built for LeviLamina on Windows x64, and the Android
LeviLauncher only loads Android libraries (`.so`). This folder is a separate mod for **LeviLauncher on Android**,
built with LeviLauncher's [preloader-android](https://github.com/LiteLDev/preloader-android) SDK.

## What this version does

Version 0.4 plays split screen on Android:

- **Player 1 keeps the touch screen.** Press Start on a controller: a new player joins with it (player 2, 3, 4) and
the screen splits. Start on a controller that already plays opens that player's pause menu. A player that leaves
can join again with Start.
- Every controller is a controller of its own: the game on Android sends every controller to player 1, the mod gives
each Android device its own controller number.
- Split screen is on: the game keeps it off on phones it thinks are too small (`AppPlatform::allowSplitScreen`).

Settings (`config/config.json`): `firstControllerIsPlayer1` gives the first controller to player 1 instead of touch.

Not ported from Windows yet: the player picker, the pause menu's Split screen menu (layout, swap, remove), the P1-P4
pictures, and the 20 second disconnect countdown. The mod still logs a lot (it is in development): everything it does
is written to `splitscreen.log` and `Download/SplitScreenMod-android.log`.

## Install

Download the newest build from [releases/android-latest](../../../releases/download/android-latest/SplitScreenMod-android-arm64.zip) (always the same link) and add it in
LeviLauncher's **Mods → Add Mod**. Start the game, enter a world, then send `Download/SplitScreenMod-android.log`.

## Build

Needs the Android NDK (28.2), CMake 3.22+ and Ninja:

```
cmake -S android -B build-android -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_HOME/ndk/28.2.13676358/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-28 -DANDROID_STL=c++_shared -DCMAKE_BUILD_TYPE=Release
cmake --build build-android --target levi_package
```

The package is `build-android/SplitScreenMod-android-arm64.zip` (also as `.levipack`).
7 changes: 7 additions & 0 deletions android/config/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"forceSplitScreen": true,
"separateControllers": true,
"forceSplitscreenJoin": true,
"firstControllerIsPlayer1": false,
"startJoins": true
}
31 changes: 31 additions & 0 deletions android/config/config.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"title": "SplitScreenMod",
"type": "object",
"properties": {
"forceSplitScreen": {
"type": "boolean",
"title": "Turn split screen on",
"description": "Turns the game's split screen on when the mod can find it for certain. Turn off if the game misbehaves."
},
"separateControllers": {
"type": "boolean",
"title": "One player per controller",
"description": "Gives each controller its own player (the game on Android sends every controller to player 1). Turn off if controllers misbehave."
},
"forceSplitscreenJoin": {
"type": "boolean",
"title": "Let other controllers join",
"description": "Lets a second, third or fourth controller join as a split-screen player. Turn off if the game misbehaves."
},
"firstControllerIsPlayer1": {
"type": "boolean",
"title": "First controller plays player 1",
"description": "Off: player 1 keeps the touch screen and every controller joins as a new player with Start. On: the first controller plays player 1."
},
"startJoins": {
"type": "boolean",
"title": "Start joins directly",
"description": "On: Start on a controller without a player adds a player in a loaded world (the game shows no join prompt on Android). Off: only the game's own join prompt adds players."
}
}
}
9 changes: 9 additions & 0 deletions android/manifest.json.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "preload-native",
"name": "@MOD_NAME@",
"author": "@MOD_AUTHOR@",
"version": "@MOD_VERSION@",
"entry": "lib@MOD_LIBRARY_NAME@.so",
"icon": "",
"minecraft_versions": @MOD_MINECRAFT_VERSIONS@
}
89 changes: 89 additions & 0 deletions android/pack/ui/pause_screen.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"namespace": "pause",

// Android: the Windows version's pause_screen.json with the Split screen button always shown (its bindings come from
// the Windows mod) and without the free-controller button. A square name-tag button in the row of small buttons
// under Save & Quit, styled like them. The mod handles "button.splitscreen_menu".
"smaller_buttons_panel/stacked_column": {
"modifications": [
{
"control_name": "fill_3",
"operation": "insert_before",
"value": [
{
"splitscreen_padding": {
"type": "panel",
"size": [ "2%", 0 ]
}
},
{
"splitscreen_small_button": {
"type": "input_panel",
"size": [ "100%c + 4px", 32 ],
"controls": [ { "splitscreen_btn@pause.splitscreen_button_small_with_tooltip": {} } ]
}
}
]
}
]
},

"splitscreen_icon": {
"type": "image",
"texture": "textures/items/name_tag",
"layer": 3,
"size": [ 16, 16 ]
},

"splitscreen_button_small@common_buttons.light_content_button": {
"$pressed_button_name": "button.splitscreen_menu",
"$focus_id": "icon_panel_main_focus",
"focus_change_down": "return_to_game",
"$button_tts_header": "splitscreen.title.menu",
"size": [ 24, 24 ],
"$button_content": "pause.splitscreen_icon",
"$pressed_alpha": 0.65
},

// the game's controller glyph, centered in the button
"splitscreen_free_icon": {
"type": "image",
"texture": "textures/ui/controller_glyph",
"layer": 3,
"size": [ 16, 16 ],
"anchor_from": "center",
"anchor_to": "center"
},

// alone in the world: take player 1's controller away so it can choose a player
"splitscreen_free_button_small@common_buttons.light_content_button": {
"$pressed_button_name": "button.splitscreen_free_controller",
"$default_button_texture": "textures/ui/button_red",
"$hover_button_texture": "textures/ui/button_red",
"$pressed_button_texture": "textures/ui/button_red_pressed",
"$focus_id": "icon_panel_main_focus",
"focus_change_down": "return_to_game",
"$button_tts_header": "splitscreen.pause.freeController",
"size": [ 24, 24 ],
"$button_content": "pause.splitscreen_free_icon",
"$pressed_alpha": 0.65
},

"splitscreen_free_button_small_with_tooltip": {
"type": "panel",
"size": [ 24, 24 ],
"controls": [
{ "button@pause.splitscreen_free_button_small": {} },
{ "tooltip_trigger@pause.icon_button_tooltip_trigger": { "$tooltip_text": "splitscreen.pause.freeController" } }
]
},

"splitscreen_button_small_with_tooltip": {
"type": "panel",
"size": [ 24, 24 ],
"controls": [
{ "button@pause.splitscreen_button_small": {} },
{ "tooltip_trigger@pause.icon_button_tooltip_trigger": { "$tooltip_text": "splitscreen.title.menu" } }
]
}
}
Loading
Loading