diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c9102556..116e10dd0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -845,6 +845,8 @@ target_sources(${PROJECT_NAME} PRIVATE core/network/net_handshake.cpp core/network/net_handshake.h core/network/net_platform.h + core/network/output.cpp + core/network/output.h core/network/picoppp.cpp core/network/picoppp.h) diff --git a/core/cfg/option.cpp b/core/cfg/option.cpp index b40d3214d..78028a7e8 100644 --- a/core/cfg/option.cpp +++ b/core/cfg/option.cpp @@ -133,6 +133,7 @@ Option GGPOAnalogAxes("GGPOAnalogAxes", 0, "network"); Option GGPOChat("GGPOChat", true, "network"); Option GGPOChatTimeoutToggle("GGPOChatTimeoutToggle", true, "network"); Option GGPOChatTimeout("GGPOChatTimeout", 10, "network"); +Option NetworkOutput("NetworkOutput", false, "network"); #ifdef SUPPORT_DISPMANX Option DispmanxMaintainAspect("maintain_aspect", true, "dispmanx"); diff --git a/core/cfg/option.h b/core/cfg/option.h index 9f868172e..0c063015e 100644 --- a/core/cfg/option.h +++ b/core/cfg/option.h @@ -491,6 +491,7 @@ extern Option GGPOAnalogAxes; extern Option GGPOChat; extern Option GGPOChatTimeoutToggle; extern Option GGPOChatTimeout; +extern Option NetworkOutput; #ifdef SUPPORT_DISPMANX extern Option DispmanxMaintainAspect; diff --git a/core/hw/maple/maple_jvs.cpp b/core/hw/maple/maple_jvs.cpp index e96081d01..7345f519e 100644 --- a/core/hw/maple/maple_jvs.cpp +++ b/core/hw/maple/maple_jvs.cpp @@ -24,6 +24,7 @@ #include "oslib/oslib.h" #include "stdclass.h" #include "cfg/option.h" +#include "network/output.h" #define LOGJVS(...) DEBUG_LOG(JVS, __VA_ARGS__) @@ -195,7 +196,23 @@ protected: } } - virtual void write_digital_out(int count, u8 *data) { } + virtual void write_digital_out(int count, u8 *data) + { + u32 newOutput = digOutput; + for (int i = 0; i < count && i < 4; i++) + { + u32 mask = 0xff << (i * 8); + newOutput = (newOutput & ~mask) | (data[i] << (i * 8)); + } + u32 changes = newOutput ^ digOutput; + for (int i = 0; i < 32; i++) + if (changes & (1 << i)) + { + std::string name = "lamp" + std::to_string(i); + networkOutput.output(name.c_str(), (newOutput >> i) & 1); + } + digOutput = newOutput; + } u32 player_count = 0; u32 digital_in_count = 0; @@ -242,6 +259,7 @@ private: std::array cur_mapping; std::array p1_mapping; std::array p2_mapping; + u32 digOutput = 0; }; // Most common JVS board @@ -1632,13 +1650,18 @@ u32 jvs_io_board::handle_jvs_message(u8 *buffer_in, u32 length_in, u8 *buffer_ou break; case 0x32: // switched outputs - case 0x33: LOGJVS("output(%d) %x", buffer_in[cmdi + 1], buffer_in[cmdi + 2]); write_digital_out(buffer_in[cmdi + 1], &buffer_in[cmdi + 2]); JVS_STATUS1(); // report byte cmdi += buffer_in[cmdi + 1] + 2; break; + case 0x33: // Analog output + LOGJVS("analog output(%d) %x", buffer_in[cmdi + 1], buffer_in[cmdi + 2]); + JVS_STATUS1(); // report byte + cmdi += buffer_in[cmdi + 1] + 2; + break; + case 0x30: // substract coin if (buffer_in[cmdi + 1] > 0 && first_player + buffer_in[cmdi + 1] - 1 < (int)ARRAY_SIZE(coin_count)) coin_count[first_player + buffer_in[cmdi + 1] - 1] -= (buffer_in[cmdi + 2] << 8) + buffer_in[cmdi + 3]; diff --git a/core/hw/naomi/naomi.cpp b/core/hw/naomi/naomi.cpp index c923e3724..3183d069c 100644 --- a/core/hw/naomi/naomi.cpp +++ b/core/hw/naomi/naomi.cpp @@ -16,6 +16,7 @@ #include "naomi_regs.h" #include "naomi_m3comm.h" #include "serialize.h" +#include "network/output.h" //#define NAOMI_COMM @@ -525,6 +526,7 @@ void naomi_reg_Init() } #endif NaomiInit(); + networkOutput.init(); } void naomi_reg_Term() @@ -540,6 +542,7 @@ void naomi_reg_Term() } #endif m3comm.closeNetwork(); + networkOutput.term(); } void naomi_reg_Reset(bool hard) @@ -578,7 +581,7 @@ void naomi_reg_Reset(bool hard) static u8 aw_maple_devs; static u64 coin_chute_time[4]; -static u8 ffbOuput; +static u8 awDigitalOuput; u32 libExtDevice_ReadMem_A0_006(u32 addr,u32 size) { addr &= 0x7ff; @@ -636,7 +639,7 @@ u32 libExtDevice_ReadMem_A0_006(u32 addr,u32 size) { // ??? Dolphin Blue return 0; case 0x28c: - return ffbOuput; + return awDigitalOuput; } INFO_LOG(NAOMI, "Unhandled read @ %x sz %d", addr, size); return 0xFF; @@ -654,11 +657,27 @@ void libExtDevice_WriteMem_A0_006(u32 addr,u32 data,u32 size) { case 0x288: // ??? Dolphin Blue return; - case 0x28C: // Wheel force feedback - // bit 0 direction (0 pos, 1 neg) - // bit 1-4 strength - ffbOuput = data; - DEBUG_LOG(NAOMI, "AW output %02x", data); + case 0x28C: // Digital output + if ((u8)data != awDigitalOuput) + { + if (atomiswaveForceFeedback) + // Wheel force feedback: + // bit 0 direction (0 pos, 1 neg) + // bit 1-4 strength + networkOutput.output("awffb", (u8)data); + else + { + u8 changes = data ^ awDigitalOuput; + for (int i = 0; i < 8; i++) + if (changes & (1 << i)) + { + std::string name = "lamp" + std::to_string(i); + networkOutput.output(name.c_str(), (data >> i) & 1); + } + } + awDigitalOuput = data; + DEBUG_LOG(NAOMI, "AW output %02x", data); + } return; default: break; @@ -780,6 +799,8 @@ static void forceFeedbackMidiReceiver(u8 data) // https://github.com/Boomslangnz/FFBArcadePlugin/blob/master/Game%20Files/Demul.cpp if (midiTxBuf[0] == 0x85 && midiTxBuf[1] == 0x3f) MapleConfigMap::UpdateVibration(0, std::max(0.f, (float)(midiTxBuf[2] - 1) / 24.f), 0.f, 5); + if (midiTxBuf[0] != 0xfd) + networkOutput.output("midiffb", (midiTxBuf[0] << 16) | (midiTxBuf[1]) << 8 | midiTxBuf[2]); } midiTxBufIndex = (midiTxBufIndex + 1) % ARRAY_SIZE(midiTxBuf); } diff --git a/core/hw/naomi/naomi_cart.cpp b/core/hw/naomi/naomi_cart.cpp index 2600e5559..de845f6bd 100644 --- a/core/hw/naomi/naomi_cart.cpp +++ b/core/hw/naomi/naomi_cart.cpp @@ -49,6 +49,7 @@ InputDescriptors *NaomiGameInputs; u8 *naomi_default_eeprom; MaxSpeedNetPipe maxSpeedNetPipe; +bool atomiswaveForceFeedback; extern MemChip *sys_rom; @@ -573,6 +574,7 @@ void naomi_cart_LoadRom(const char* file, LoadProgress *progress) else loadDecryptedRom(file, progress); + atomiswaveForceFeedback = false; RomBootID bootId; if (CurrentCartridge->GetBootId(&bootId)) { @@ -593,6 +595,11 @@ void naomi_cart_LoadRom(const char* file, LoadProgress *progress) { maxSpeedNetPipe.init(); configure_maxspeed_flash(config::NetworkEnable, config::ActAsServer); + atomiswaveForceFeedback = true; + } + else if (gameId == "FASTER THAN SPEED") + { + atomiswaveForceFeedback = true; } else if (gameId == "SAMPLE GAME MAX LONG NAME-") // Driving Simulator { diff --git a/core/hw/naomi/naomi_cart.h b/core/hw/naomi/naomi_cart.h index e5114aa1a..9f0f6fa8b 100644 --- a/core/hw/naomi/naomi_cart.h +++ b/core/hw/naomi/naomi_cart.h @@ -166,3 +166,4 @@ struct InputDescriptors }; extern InputDescriptors *NaomiGameInputs; +extern bool atomiswaveForceFeedback; diff --git a/core/network/output.cpp b/core/network/output.cpp new file mode 100644 index 000000000..6e34a01f7 --- /dev/null +++ b/core/network/output.cpp @@ -0,0 +1,21 @@ +/* + Copyright 2022 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#include "output.h" + +NetworkOutput networkOutput; diff --git a/core/network/output.h b/core/network/output.h new file mode 100644 index 000000000..7cc5b9aca --- /dev/null +++ b/core/network/output.h @@ -0,0 +1,115 @@ +/* + Copyright 2022 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . + */ +#pragma once +#include "types.h" +#include "net_platform.h" +#include "emulator.h" +#include "cfg/option.h" + +class NetworkOutput +{ +public: + void init() + { + if (!config::NetworkOutput) + return; + server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + int option = 1; + setsockopt(server, SOL_SOCKET, SO_REUSEADDR, (const char *)&option, sizeof(option)); + + sockaddr_in saddr{}; + socklen_t saddr_len = sizeof(saddr); + saddr.sin_family = AF_INET; + saddr.sin_addr.s_addr = INADDR_ANY; + saddr.sin_port = htons(8000); + if (::bind(server, (sockaddr *)&saddr, saddr_len) < 0) + { + perror("bind"); + term(); + return; + } + if (listen(server, 5) < 0) + { + perror("listen"); + term(); + return; + } + set_non_blocking(server); + EventManager::listen(Event::VBlank, vblankCallback, this); + } + + void term() + { + EventManager::unlisten(Event::VBlank, vblankCallback, this); + for (sock_t sock : clients) + closesocket(sock); + clients.clear(); + if (server != INVALID_SOCKET) + { + closesocket(server); + server = INVALID_SOCKET; + } + } + + void output(const char *name, u32 value) + { + if (!config::NetworkOutput) + return; + char s[9]; + sprintf(s, "%x", value); + std::string msg = std::string(name) + " = " + std::string(s) + "\n"; // mame uses \r + std::vector errorSockets; + for (sock_t sock : clients) + if (::send(sock, msg.c_str(), msg.length(), 0) < 0) + { + int error = get_last_error(); + if (error != L_EWOULDBLOCK && error != L_EAGAIN) + errorSockets.push_back(sock); + } + for (sock_t sock : errorSockets) + { + closesocket(sock); + clients.erase(std::find(clients.begin(), clients.end(), sock)); + } + } + +private: + static void vblankCallback(Event event, void *param) { + ((NetworkOutput *)param)->acceptConnections(); + } + + void acceptConnections() + { + sockaddr_in src_addr{}; + socklen_t addr_len = sizeof(src_addr); + sock_t sockfd = accept(server, (sockaddr *)&src_addr, &addr_len); + if (sockfd != INVALID_SOCKET) + { + set_non_blocking(sockfd); + clients.push_back(sockfd); + } + } + + sock_t server = INVALID_SOCKET; + std::vector clients; +}; + +extern NetworkOutput networkOutput; + diff --git a/core/rend/gui.cpp b/core/rend/gui.cpp index 1a1878c95..75f3a654e 100644 --- a/core/rend/gui.cpp +++ b/core/rend/gui.cpp @@ -2142,9 +2142,9 @@ static void gui_display_settings() ShowHelpMarker("The local UDP port to use"); config::LocalPort.set(atoi(localPort)); } - OptionCheckbox("Enable UPnP", config::EnableUPnP); - ImGui::SameLine(); - ShowHelpMarker("Automatically configure your network router for netplay"); + OptionCheckbox("Enable UPnP", config::EnableUPnP, "Automatically configure your network router for netplay"); + OptionCheckbox("Broadcast Digital Outputs", config::NetworkOutput, "Broadcast digital outputs and force-feedback state on TCP port 8000. " + "Compatible with the \"-output network\" MAME option. Arcade games only."); } ImGui::Spacing(); header("Other"); @@ -2353,6 +2353,7 @@ static void gui_display_content() gui_start_game(""); ImGui::PopID(); int counter = 1; + int loadedImages = 0; { scanner.get_mutex().lock(); for (const auto& game : scanner.get_game_list()) @@ -2393,8 +2394,10 @@ static void gui_display_content() { // Get the boxart texture. Load it if needed. textureId = imguiDriver->getTexture(art->boxartPath); - if (textureId == ImTextureID()) + if (textureId == ImTextureID() && loadedImages < 10) { + // Load 10 images max per frame + loadedImages++; int width, height; u8 *imgData = loadImage(art->boxartPath, width, height); if (imgData != nullptr) diff --git a/shell/libretro/libretro_core_options.h b/shell/libretro/libretro_core_options.h index 6d7d56993..8a4aefc66 100644 --- a/shell/libretro/libretro_core_options.h +++ b/shell/libretro/libretro_core_options.h @@ -740,6 +740,20 @@ struct retro_core_option_v2_definition option_defs_us[] = { }, "enabled", }, + { + CORE_OPTION_NAME "_network_output", + "Broadcast Digital Outputs", + NULL, + "Broadcast digital outputs and force-feedback state on TCP port 8000. Compatible with the \"-output network\" MAME option.", + NULL, + "input", + { + { "disabled", NULL }, + { "enabled", NULL }, + { NULL, NULL }, + }, + "disabled", + }, { CORE_OPTION_NAME "_show_lightgun_settings", "Show Light Gun Settings", diff --git a/shell/libretro/option.cpp b/shell/libretro/option.cpp index 564282128..6412bdc10 100644 --- a/shell/libretro/option.cpp +++ b/shell/libretro/option.cpp @@ -117,6 +117,7 @@ Option GGPOEnable("", false); Option GGPODelay("", 0); Option NetworkStats("", false); Option GGPOAnalogAxes("", 0); +Option NetworkOutput(CORE_OPTION_NAME "_network_output", false); // Maple