diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 4a598db..7091718 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -29,8 +29,8 @@ android { minSdk = 26 targetSdk = 36 - versionCode = 20 - versionName = "1.1.9" + versionCode = 21 + versionName = "1.2.0" ndk { abiFilters += listOf("arm64-v8a") diff --git a/android/app/src/main/assets/Config/Supermodel.ini b/android/app/src/main/assets/Config/Supermodel.ini index d6e31ec..7d7824a 100644 --- a/android/app/src/main/assets/Config/Supermodel.ini +++ b/android/app/src/main/assets/Config/Supermodel.ini @@ -18,8 +18,14 @@ SoundVolume = 100 MusicVolume = 150 LegacySoundDSP = 0 ForceFeedback = 0 +; Net board TCP link settings (for Android<->PC or Android<->Android). +; Cab A default: PortIn=1970 PortOut=1971 +; Cab B default: PortIn=1971 PortOut=1970 and AddressOut= Network = 0 -SimulateNet = 0 +SimulateNet = 1 +PortIn = 1970 +PortOut = 1971 +AddressOut = "127.0.0.1" Outputs = none LegacyReal3DTiming = 1 diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt index c6780e0..ab5d0c4 100644 --- a/android/app/src/main/cpp/CMakeLists.txt +++ b/android/app/src/main/cpp/CMakeLists.txt @@ -56,7 +56,7 @@ set(EXCLUDE_REGEX "/Src/Model3/53C810Disasm\\.cpp$|" "/Src/OSD/.*\\.(c|cpp)$|" "/Src/Graphics/.*\\.(c|cpp)$|" - "/Src/Network/.*\\.(c|cpp)$|" + "/Src/Network/TCPSendAsync\\.cpp$|" "/Src/Sound/SCSPLFO\\.cpp$|" "/Src/Pkgs/GL/.*\\.(c|cpp|h)$|" "/Src/Pkgs/glew\\.c$|" @@ -77,7 +77,7 @@ foreach(src ${SUPER3_SOURCES}) OR src MATCHES "/Src/Model3/53C810Disasm\\.cpp$" OR src MATCHES "/Src/OSD/" OR src MATCHES "/Src/Graphics/" - OR src MATCHES "/Src/Network/" + OR src MATCHES "/Src/Network/TCPSendAsync\\.cpp$" OR src MATCHES "/Src/Sound/SCSPLFO\\.cpp$" OR src MATCHES "/Src/Util/Test_" OR src MATCHES "/Src/Pkgs/GL/" @@ -95,6 +95,8 @@ endif() # latest release tarball. option(SUPER3_FETCH_SDL2 "Download and build SDL2 for Android" ON) set(SDL2_VERSION "2.32.10") +option(SUPER3_FETCH_SDL2_NET "Download and build SDL2_net for Android" ON) +set(SDL2_NET_VERSION "2.2.0") if(DEFINED SDL2_LOCAL_DIR) message(STATUS "Using local SDL2 at ${SDL2_LOCAL_DIR}") @@ -107,6 +109,29 @@ elseif(SUPER3_FETCH_SDL2) FetchContent_MakeAvailable(SDL2) endif() +if(DEFINED SDL2_NET_LOCAL_DIR) + message(STATUS "Using local SDL2_net at ${SDL2_NET_LOCAL_DIR}") + add_subdirectory("${SDL2_NET_LOCAL_DIR}" "${CMAKE_BINARY_DIR}/sdl2-net-local") +elseif(SUPER3_FETCH_SDL2_NET) + FetchContent_Declare( + SDL2_NET + URL "https://github.com/libsdl-org/SDL_net/archive/refs/tags/release-${SDL2_NET_VERSION}.zip" + ) + FetchContent_MakeAvailable(SDL2_NET) +endif() + +set(SDL2_NET_TARGET "") +if(TARGET SDL2_net::SDL2_net) + set(SDL2_NET_TARGET SDL2_net::SDL2_net) +elseif(TARGET SDL2_net::SDL2_net-static) + set(SDL2_NET_TARGET SDL2_net::SDL2_net-static) +elseif(TARGET SDL2_net) + set(SDL2_NET_TARGET SDL2_net) +endif() +if(SDL2_NET_TARGET STREQUAL "") + message(FATAL_ERROR "SDL2_net target not found. Enable SUPER3_FETCH_SDL2_NET or provide SDL2_NET_LOCAL_DIR.") +endif() + add_library(super3 SHARED native-lib.cpp android_shim.cpp @@ -132,7 +157,7 @@ add_library(super3 SHARED ${M68K_GENERATED_SOURCES} ) -target_compile_definitions(super3 PRIVATE ANDROID __ANDROID__) +target_compile_definitions(super3 PRIVATE ANDROID __ANDROID__ NET_BOARD) target_include_directories(super3 PRIVATE "${REPO_ROOT}/Src" @@ -154,4 +179,5 @@ target_link_libraries(super3 PRIVATE ${egl-lib} ${glesv3-lib} SDL2::SDL2 # present when SUPER3_FETCH_SDL2 is ON + ${SDL2_NET_TARGET} ) diff --git a/android/app/src/main/cpp/native-lib.cpp b/android/app/src/main/cpp/native-lib.cpp index 6ee5b1e..ee2b0ab 100644 --- a/android/app/src/main/cpp/native-lib.cpp +++ b/android/app/src/main/cpp/native-lib.cpp @@ -336,7 +336,10 @@ struct Super3Host { config.Set("Outputs", "none"); config.Set("ForceFeedback", false); config.Set("Network", false); - config.Set("SimulateNet", false); + config.Set("SimulateNet", true); + config.Set("PortIn", unsigned(1970)); + config.Set("PortOut", unsigned(1971)); + config.Set("AddressOut", "127.0.0.1"); config.Set("XResolution", "496"); config.Set("YResolution", "384"); config.Set("Throttle", true); @@ -359,7 +362,7 @@ struct Super3Host { config.Set("InputJoyRight", "KEY_RIGHT"); config.Set("InputSteeringLeft", "KEY_LEFT"); config.Set("InputSteeringRight", "KEY_RIGHT"); - config.Set("InputAccelerator", "KEY_W"); + config.Set("InputAccelerator", "KEY_W"); config.Set("InputBrake", "KEY_S"); config.Set("UISaveState", "KEY_F5"); config.Set("UIChangeSlot", "KEY_F6"); @@ -401,8 +404,6 @@ struct Super3Host { config.Set("InputSystem", "sdl"); config.Set("Outputs", "none"); config.Set("ForceFeedback", false); - config.Set("Network", false); - config.Set("SimulateNet", false); // Ensure required nodes exist / sane. config.Set("PowerPCFrequency", "50"); @@ -431,6 +432,41 @@ struct Super3Host { config.Set("YResolution", std::to_string(yRes)); } + // Ensure netboard keys exist and values are sane. The core expects these + // keys to exist when NET_BOARD is enabled. + { + bool networkEnabled = false; + bool simulateNet = true; + try { networkEnabled = config["Network"].ValueAsDefault(false); } catch (...) { networkEnabled = false; } + try { simulateNet = config["SimulateNet"].ValueAsDefault(true); } catch (...) { simulateNet = true; } + + unsigned portIn = 1970; + unsigned portOut = 1971; + try { portIn = config["PortIn"].ValueAsDefault(1970); } catch (...) { portIn = 1970; } + try { portOut = config["PortOut"].ValueAsDefault(1971); } catch (...) { portOut = 1971; } + if (portIn == 0 || portIn > 65535) portIn = 1970; + if (portOut == 0 || portOut > 65535) portOut = 1971; + + std::string addressOut = config["AddressOut"].ValueAsDefault("127.0.0.1"); + if (addressOut.size() >= 2 && addressOut.front() == '"' && addressOut.back() == '"') + addressOut = addressOut.substr(1, addressOut.size() - 2); + if (addressOut.empty()) + addressOut = "127.0.0.1"; + + config.Set("Network", networkEnabled); + config.Set("SimulateNet", simulateNet); + config.Set("PortIn", portIn); + config.Set("PortOut", portOut); + config.Set("AddressOut", addressOut); + + // Full netboard emulation remains thread-sensitive. Keep the Android + // default (simulated netboard) for easier, stable online setup. + if (networkEnabled && !simulateNet) { + config.Set("MultiThreaded", false); + config.Set("GPUMultiThreaded", false); + } + } + // Ensure touch zones always have a working keyboard mapping even if the user remaps to joystick-only. ensureKeyboardFallback("InputCoin1", "KEY_5"); ensureKeyboardFallback("InputStart1", "KEY_1"); @@ -573,6 +609,19 @@ struct Super3Host { ApplyAndroidHardOverrides(); ApplyGameHardOverrides(game.name); ApplyAndroidTileBlit(); + if (config["Network"].ValueAsDefault(false)) { + const bool simulate = config["SimulateNet"].ValueAsDefault(true); + const unsigned portIn = config["PortIn"].ValueAsDefault(1970); + const unsigned portOut = config["PortOut"].ValueAsDefault(1971); + const std::string addressOut = config["AddressOut"].ValueAsDefault("127.0.0.1"); + SDL_Log("NetBoard enabled (%s): PortIn=%u PortOut=%u AddressOut=%s", + simulate ? "simulated" : "emulated", + portIn, + portOut, + addressOut.c_str()); + } else { + SDL_Log("NetBoard disabled."); + } // Initialize inputs before attaching to model if (!inputs.Initialize()) { diff --git a/android/app/src/main/java/com/izzy2lost/super3/IniSettingsActivity.kt b/android/app/src/main/java/com/izzy2lost/super3/IniSettingsActivity.kt index e450b72..69f298e 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/IniSettingsActivity.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/IniSettingsActivity.kt @@ -23,6 +23,11 @@ class IniSettingsActivity : AppCompatActivity() { private lateinit var btnEmulateSound: MaterialButton private lateinit var btnSoundVolume: MaterialButton private lateinit var btnMusicVolume: MaterialButton + private lateinit var btnNetworkEnabled: MaterialButton + private lateinit var btnSimulateNet: MaterialButton + private lateinit var btnAddressOut: MaterialButton + private lateinit var btnPortIn: MaterialButton + private lateinit var btnPortOut: MaterialButton private var iniDoc: DocumentFile? = null private var iniLines: MutableList = mutableListOf() @@ -41,6 +46,11 @@ class IniSettingsActivity : AppCompatActivity() { btnEmulateSound = findViewById(R.id.btn_emulate_sound) btnSoundVolume = findViewById(R.id.btn_sound_volume) btnMusicVolume = findViewById(R.id.btn_music_volume) + btnNetworkEnabled = findViewById(R.id.btn_network_enabled) + btnSimulateNet = findViewById(R.id.btn_simulate_net) + btnAddressOut = findViewById(R.id.btn_address_out) + btnPortIn = findViewById(R.id.btn_port_in) + btnPortOut = findViewById(R.id.btn_port_out) toolbar.setNavigationOnClickListener { finish() } onBackPressedDispatcher.addCallback(this) { finish() } @@ -125,6 +135,55 @@ class IniSettingsActivity : AppCompatActivity() { max = 200, ) } + + btnNetworkEnabled.setOnClickListener { + val enabled = btnNetworkEnabled.isChecked + applyUpdate( + updates = mapOf("Network" to if (enabled) "1" else "0"), + onApplied = { btnNetworkEnabled.isChecked = enabled }, + onFailed = { btnNetworkEnabled.isChecked = !enabled }, + ) + } + + btnSimulateNet.setOnClickListener { + val enabled = btnSimulateNet.isChecked + applyUpdate( + updates = mapOf("SimulateNet" to if (enabled) "1" else "0"), + onApplied = { btnSimulateNet.isChecked = enabled }, + onFailed = { btnSimulateNet.isChecked = !enabled }, + ) + } + + btnAddressOut.setOnClickListener { + val current = readIniStringUnquoted("AddressOut").orEmpty().ifBlank { "127.0.0.1" } + showTextDialog( + title = "Target address (AddressOut)", + key = "AddressOut", + current = current, + ) + } + + btnPortIn.setOnClickListener { + val current = readIniInt("PortIn") ?: 1970 + showNumberDialog( + title = "Listen port (PortIn)", + key = "PortIn", + current = current, + min = 1, + max = 65535, + ) + } + + btnPortOut.setOnClickListener { + val current = readIniInt("PortOut") ?: 1971 + showNumberDialog( + title = "Send port (PortOut)", + key = "PortOut", + current = current, + min = 1, + max = 65535, + ) + } } private fun loadIni(treeUri: Uri) { @@ -160,6 +219,18 @@ class IniSettingsActivity : AppCompatActivity() { val music = readIniInt("MusicVolume") ?: 150 btnSoundVolume.text = "Sound volume: $sound" btnMusicVolume.text = "Music volume: $music" + + val networkEnabled = readIniBool("Network") ?: false + val simulateNet = readIniBool("SimulateNet") ?: true + val portIn = readIniInt("PortIn")?.coerceIn(1, 65535) ?: 1970 + val portOut = readIniInt("PortOut")?.coerceIn(1, 65535) ?: 1971 + val addressOut = readIniStringUnquoted("AddressOut").orEmpty().ifBlank { "127.0.0.1" } + + btnNetworkEnabled.isChecked = networkEnabled + btnSimulateNet.isChecked = simulateNet + btnAddressOut.text = "Address out: $addressOut" + btnPortIn.text = "Listen port (PortIn): $portIn" + btnPortOut.text = "Send port (PortOut): $portOut" } private fun showNumberDialog(title: String, key: String, current: Int, min: Int, max: Int) { @@ -192,6 +263,8 @@ class IniSettingsActivity : AppCompatActivity() { "PowerPCFrequency" -> btnPpcFrequency.text = "PowerPC frequency: $clamped" "SoundVolume" -> btnSoundVolume.text = "Sound volume: $clamped" "MusicVolume" -> btnMusicVolume.text = "Music volume: $clamped" + "PortIn" -> btnPortIn.text = "Listen port (PortIn): $clamped" + "PortOut" -> btnPortOut.text = "Send port (PortOut): $clamped" } }, ) @@ -200,6 +273,38 @@ class IniSettingsActivity : AppCompatActivity() { .show() } + private fun showTextDialog(title: String, key: String, current: String) { + val view = layoutInflater.inflate(R.layout.dialog_number_input, null) + val inputLayout = view.findViewById(R.id.number_input_layout) + val input = view.findViewById(R.id.number_input) + inputLayout.hint = title + input.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI + input.setText(current) + input.setSelection(input.text?.length ?: 0) + + MaterialAlertDialogBuilder(this) + .setTitle(title) + .setView(view) + .setPositiveButton("Save") { _, _ -> + val raw = input.text?.toString()?.trim().orEmpty() + val value = raw.trim('"') + if (value.isBlank()) { + Toast.makeText(this, "Address cannot be empty", Toast.LENGTH_SHORT).show() + return@setPositiveButton + } + applyUpdate( + updates = mapOf(key to "\"$value\""), + onApplied = { + if (key == "AddressOut") { + btnAddressOut.text = "Address out: $value" + } + }, + ) + } + .setNegativeButton("Cancel", null) + .show() + } + private fun applyUpdate( updates: Map, onApplied: () -> Unit, @@ -253,6 +358,10 @@ class IniSettingsActivity : AppCompatActivity() { return null } + private fun readIniStringUnquoted(key: String): String? { + return readIniString(key)?.trim()?.trim('"') + } + private fun updateIniSection( lines: List, section: String, @@ -345,5 +454,10 @@ class IniSettingsActivity : AppCompatActivity() { btnEmulateSound.isEnabled = enabled btnSoundVolume.isEnabled = enabled btnMusicVolume.isEnabled = enabled + btnNetworkEnabled.isEnabled = enabled + btnSimulateNet.isEnabled = enabled + btnAddressOut.isEnabled = enabled + btnPortIn.isEnabled = enabled + btnPortOut.isEnabled = enabled } } diff --git a/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt b/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt index 677403f..f4b9fa3 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt @@ -92,6 +92,7 @@ class Super3Activity : SDLActivity() { override fun getLibraries(): Array = arrayOf( "SDL2", + "SDL2_net", "super3", ) diff --git a/android/app/src/main/res/layout/activity_ini_settings.xml b/android/app/src/main/res/layout/activity_ini_settings.xml index af8bac7..0055624 100644 --- a/android/app/src/main/res/layout/activity_ini_settings.xml +++ b/android/app/src/main/res/layout/activity_ini_settings.xml @@ -128,6 +128,57 @@ android:text="Music volume: 150" style="?attr/materialButtonElevatedStyle" /> + + + + + + + + + + + +