diff --git a/desktop/src/com/unciv/app/desktop/AwtClipboard.kt b/desktop/src/com/unciv/app/desktop/AwtClipboard.kt new file mode 100644 index 0000000000..dc29e23e3f --- /dev/null +++ b/desktop/src/com/unciv/app/desktop/AwtClipboard.kt @@ -0,0 +1,42 @@ +package com.unciv.app.desktop + +import java.awt.Toolkit +import java.awt.datatransfer.DataFlavor +import java.awt.datatransfer.StringSelection +import java.awt.datatransfer.Clipboard as ClipboardAwt +import com.badlogic.gdx.utils.Clipboard as ClipboardGdx + +/** + * A plug-in replacement for Gdx Lwjgl3Clipboard that goes through AWT instead of GLFW and removes the stack size limitation. + * + * Gdx.app.clipboard on desktop is a Lwjgl3Clipboard instance, which allocates a buffer on the stack for UTF8 conversion at the Lwjgl3-GLFW interface. + * The default stack size is a severe limitation, which we **originally** treated by increasing the limit, which can only be done statically at launch time: +``` + // 386 is an almost-arbitrary choice from the saves I had at the moment and their GZipped size. + // There must be a reason for lwjgl3 being so stingy, which for me meant to stay conservative. + System.setProperty("org.lwjgl.system.stackSize", "384") +``` + * - See [setContents](https://github.com/libgdx/libgdx/blob/master/backends/gdx-backend-lwjgl3/src/com/badlogic/gdx/backends/lwjgl3/Lwjgl3Clipboard.java#L40) + * - See [glfwSetClipboardString](https://github.com/LWJGL/lwjgl3/blob/master/modules/lwjgl/glfw/src/generated/java/org/lwjgl/glfw/GLFW.java#L5068-L5077) + * - See [Higher available clipboard size](https://github.com/orgs/LWJGL/discussions/769) + */ +class AwtClipboard : ClipboardGdx { + // A lazy seems to work too, but not keeping a reference when not active is safer (also debuggable while a lazy is not): + private val clipboard: ClipboardAwt + get() = Toolkit.getDefaultToolkit().systemClipboard + + override fun hasContents(): Boolean { + return DataFlavor.stringFlavor in clipboard.availableDataFlavors + } + + override fun getContents(): String? { + val transferable = clipboard.getContents(null) + if (!transferable.isDataFlavorSupported(DataFlavor.stringFlavor)) return null + return transferable.getTransferData(DataFlavor.stringFlavor) as String + } + + override fun setContents(content: String?) { + val selection = StringSelection(content) // Yes this supports null + clipboard.setContents(selection, selection) + } +} diff --git a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt index 8cee48dc88..87355716ee 100644 --- a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt @@ -36,10 +36,6 @@ internal object DesktopLauncher { // Solves a rendering problem in specific GPUs and drivers. // For more info see https://github.com/yairm210/Unciv/pull/3202 and https://github.com/LWJGL/lwjgl/issues/119 System.setProperty("org.lwjgl.opengl.Display.allowSoftwareOpenGL", "true") - // This setting (default 64) limits clipboard transfers. Value in kB! - // 386 is an almost-arbitrary choice from the saves I had at the moment and their GZipped size. - // There must be a reason for lwjgl3 being so stingy, which for me meant to stay conservative. - System.setProperty("org.lwjgl.system.stackSize", "384") val isRunFromJAR = DesktopLauncher.javaClass.`package`.specificationVersion != null ImagePacker.packImages(isRunFromJAR) diff --git a/desktop/src/com/unciv/app/desktop/HardenGdxAudio.kt b/desktop/src/com/unciv/app/desktop/HardenGdxAudio.kt index f4b7501cf2..60d729cea9 100644 --- a/desktop/src/com/unciv/app/desktop/HardenGdxAudio.kt +++ b/desktop/src/com/unciv/app/desktop/HardenGdxAudio.kt @@ -6,10 +6,18 @@ import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration import com.badlogic.gdx.backends.lwjgl3.audio.OpenALLwjgl3Audio import com.badlogic.gdx.backends.lwjgl3.audio.OpenALMusic import com.badlogic.gdx.utils.Array +import com.badlogic.gdx.utils.Clipboard import com.unciv.ui.audio.MusicController /** - * Problem: Not all exceptions playing Music can be caught on the desktop platform using a try-catch around the play method. + * ### Notes + * - Since this is the one replacement wrapping Lwjgl3Application that will be running Unciv-desktop, and we want to replace Gdx.app.clipboard, + * the hook for [AwtClipboard] is implemented here as [getClipboard] override. + * - ***This class is never properly initialized before use!*** (because the super constructor runs until the game is quit.) + * Therefore, fields must be initialized on-demand or from upstream UncivGame + * + * ### Problem + * Not all exceptions playing Music can be caught on the desktop platform using a try-catch around the play method. * Unciv 3.17.13 to 4.0.5 all exacerbated the problem due to using Music from various threads - and Gdx documents it isn't threadsafe. * But even with that fixed, music streams can have codec failures _after_ the first buffer's worth of data, so the problem is only mitigated. * @@ -18,7 +26,7 @@ import com.unciv.ui.audio.MusicController * * This catches those Exceptions and reports them through a callback mechanism, and also provides a callback from the app loop * that allows MusicController to make its Music calls on a thread guaranteed to be safe for OpenALMusic. - * # + * * ### Approach: * - Subclass [OpenALLwjgl3Audio] overriding [update][OpenALLwjgl3Audio.update] with equivalent code catching any Exceptions and Errors * - Get the framework to use the subclassed Audio by overriding Lwjgl3ApplicationBase.createAudio @@ -46,6 +54,7 @@ class HardenGdxAudio( ) : Lwjgl3Application(game, config) { private var updateCallback: (()->Unit)? = null private var exceptionHandler: ((Throwable, Music)->Unit)? = null + private lateinit var awtClipboard: AwtClipboard // normal initialization won't run, including initializing lazy delegates! /** Hooks part 1 * @@ -69,6 +78,12 @@ class HardenGdxAudio( this.exceptionHandler = exceptionHandler } + /** This redirects `Gdx.app.clipboard` on desktop to our [AwtClipboard] replacement */ + override fun getClipboard(): Clipboard { + if (!::awtClipboard.isInitialized) awtClipboard = AwtClipboard() + return awtClipboard + } + /** * Desktop implementation of the [Audio][com.badlogic.gdx.Audio] interface that * unlike its superclass catches exceptions and allows passing them into application code for handling.