Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Screen Capturing #771

Merged
merged 20 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c0423bf
Starts to add screen capture support
hobnob Oct 9, 2024
e08c1e9
Updates capture support so it's now available in the FrameContext
hobnob Oct 10, 2024
64076f6
Updates unit tests with renderer implementation
hobnob Oct 11, 2024
0505d70
Changes output of capture to an image type rather than a byte array
hobnob Oct 11, 2024
ed4ac26
Allows the data URL to be retrieved from an ImageData instance
hobnob Oct 11, 2024
8374069
Minor updates
hobnob Oct 14, 2024
3bbcd52
Improves screen capture functionality and adds a new scene to the san…
hobnob Oct 16, 2024
6fa99c9
Updates clipping calculations for screen capture
hobnob Oct 16, 2024
014f92f
Fixes clipping support when capturing the screen
hobnob Oct 17, 2024
3902783
Adds the assets that were loaded as part of the AssetBatchLoaded event
hobnob Oct 18, 2024
ecf17c5
Adds comments to the captureScreen method in Renderer.scala
hobnob Oct 17, 2024
0157e68
Capturing an image can now extract an asset
hobnob Oct 21, 2024
bb6ea66
Fixes a race condition between rebuilding the asset texture atlas and…
hobnob Oct 22, 2024
3d0c1a8
Changes default capture to WebP rather than PNG
hobnob Oct 22, 2024
15f78b3
Fixes unit tests
hobnob Oct 22, 2024
8c57ffe
Improves screen capture API to accept configuration
hobnob Oct 23, 2024
0a6665e
Changes `Some` to `Option` to account for `null` values
hobnob Nov 7, 2024
729b504
Adds a filter to screen layers before attempting to assign tags to them
hobnob Nov 7, 2024
61198b5
Moves renderer out of FrameContext and replaces it with a method pass…
hobnob Nov 7, 2024
743aadf
Fixes unit tests
hobnob Nov 7, 2024
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
Empty file modified indigo/build.sh
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ final class AssetBundleLoader[Model] extends SubSystem[Model]:
}

// Asset Response Events
case AssetEvent.AssetBatchLoaded(key, true) if tracker.containsBundle(key) =>
case AssetEvent.AssetBatchLoaded(key, _, true) if tracker.containsBundle(key) =>
Outcome(tracker)
.addGlobalEvents(AssetBundleLoaderEvent.Success(key))

case AssetEvent.AssetBatchLoaded(key, false) if tracker.containsAssetFromKey(key) =>
case AssetEvent.AssetBatchLoaded(key, _, false) if tracker.containsAssetFromKey(key) =>
// In this case the "batch" will consist of one item and
// the BindingKey is actually the AssetPath value and we
// know the asset is in one of our bundles.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class AssetBundleLoaderTests extends munit.FunSuite {
)

test("AssetBundleLoader - Journey (happy path)") {

val loader = AssetBundleLoader[Unit]
val tracker = AssetBundleTracker.empty

Expand All @@ -40,28 +39,51 @@ class AssetBundleLoaderTests extends munit.FunSuite {

// As each asset comes in, the status is checked and events are emitted.
val nextLoader1 =
loader.update(context(0), loadOutcome.unsafeGet)(AssetEvent.AssetBatchLoaded(BindingKey("/image_1.png"), false))
loader.update(context(0), loadOutcome.unsafeGet)(
AssetEvent.AssetBatchLoaded(
BindingKey("/image_1.png"),
Set(defaultAssets.find(a => a.path == AssetPath("/image_1.png")).get),
false
)
)
assertEquals(nextLoader1.unsafeGlobalEvents.length, 1)
assertEquals(nextLoader1.unsafeGlobalEvents.contains(AssetBundleLoaderEvent.LoadProgress(key, 33, 1, 3)), true)

val nextLoader2 =
loader.update(context(0), nextLoader1.unsafeGet)(AssetEvent.AssetBatchLoaded(BindingKey("/image_2.png"), false))
loader.update(context(0), nextLoader1.unsafeGet)(
AssetEvent.AssetBatchLoaded(
BindingKey("/image_2.png"),
Set(defaultAssets.find(a => a.path == AssetPath("/image_2.png")).get),
false
)
)
assertEquals(nextLoader2.unsafeGlobalEvents.length, 1)
assertEquals(nextLoader2.unsafeGlobalEvents.contains(AssetBundleLoaderEvent.LoadProgress(key, 67, 2, 3)), true)

// Eventually all assets are loaded individually, and an event is emmitted to
// load the whole bundle and also to process it.
val nextLoader3 =
loader.update(context(0), nextLoader2.unsafeGet)(AssetEvent.AssetBatchLoaded(BindingKey("/image_3.png"), false))
loader.update(context(0), nextLoader2.unsafeGet)(
AssetEvent.AssetBatchLoaded(
BindingKey("/image_3.png"),
Set(defaultAssets.find(a => a.path == AssetPath("/image_3.png")).get),
false
)
)
assertEquals(nextLoader3.unsafeGlobalEvents.length, 2)
assertEquals(nextLoader3.unsafeGlobalEvents.contains(AssetBundleLoaderEvent.LoadProgress(key, 100, 3, 3)), true)
assertEquals(
nextLoader3.unsafeGlobalEvents.contains(AssetEvent.LoadAssetBatch(defaultAssets.toSet, key, true)),
true
)

// Once the whole bundle has finished, a completion event is emitted.
val finalLoader = loader.update(context(0), nextLoader3.unsafeGet)(AssetEvent.AssetBatchLoaded(key, true))
val finalLoader = loader.update(context(0), nextLoader3.unsafeGet)(
AssetEvent.AssetBatchLoaded(
key,
defaultAssets.find(a => a.path == AssetPath(key.toString())).map(Set(_)).getOrElse(Set.empty),
true
)
)
assertEquals(finalLoader.unsafeGlobalEvents.length, 1)
assertEquals(finalLoader.unsafeGlobalEvents.contains(AssetBundleLoaderEvent.Success(key)), true)
val asset2 = finalLoader.unsafeGet.findBundleByKey(key).get.giveAssetLoadState(AssetPath("/image_2.png")).get
Expand All @@ -76,7 +98,6 @@ class AssetBundleLoaderTests extends munit.FunSuite {
val tracker = AssetBundleTracker.empty

val key = BindingKey("test")

// Someone requests that a bundle of assets be loaded.
val loadOutcome = loader.update(context(0), tracker)(AssetBundleLoaderEvent.Load(key, defaultAssets.toSet))

Expand All @@ -89,18 +110,27 @@ class AssetBundleLoaderTests extends munit.FunSuite {
true
)
assertEquals(loadOutcome.unsafeGlobalEvents.contains(AssetBundleLoaderEvent.Started(key)), true)

// As each asset comes in, the status is checked and events are emitted.
val nextLoader1 =
loader.update(context(0), loadOutcome.unsafeGet)(AssetEvent.AssetBatchLoaded(BindingKey("/image_1.png"), false))
loader.update(context(0), loadOutcome.unsafeGet)(
AssetEvent.AssetBatchLoaded(
BindingKey("/image_1.png"),
Set(defaultAssets.find(a => a.path == AssetPath("/image_1.png")).get),
false
)
)
assertEquals(nextLoader1.unsafeGlobalEvents.length, 1)
assertEquals(nextLoader1.unsafeGlobalEvents.contains(AssetBundleLoaderEvent.LoadProgress(key, 33, 1, 3)), true)

val nextLoader2 =
loader.update(context(0), nextLoader1.unsafeGet)(AssetEvent.AssetBatchLoaded(BindingKey("/image_2.png"), false))
loader.update(context(0), nextLoader1.unsafeGet)(
AssetEvent.AssetBatchLoaded(
BindingKey("/image_2.png"),
Set(defaultAssets.find(a => a.path == AssetPath("/image_2.png")).get),
false
)
)
assertEquals(nextLoader2.unsafeGlobalEvents.length, 1)
assertEquals(nextLoader2.unsafeGlobalEvents.contains(AssetBundleLoaderEvent.LoadProgress(key, 67, 2, 3)), true)

// All assets are loaded individually, but one of them fails.
val nextLoader3 = loader.update(context(0), nextLoader2.unsafeGet)(
AssetEvent.AssetBatchLoadError(BindingKey("/image_3.png"), "error")
Expand All @@ -113,7 +143,6 @@ class AssetBundleLoaderTests extends munit.FunSuite {
),
true
)

// Eventually the whole bundle is complete, but in a failed state, and
// a completion event is emitted listing the errors.
val finalLoader = loader.update(context(0), nextLoader3.unsafeGet)(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package indigoextras.ui

import indigo.platform.assets.DynamicText
import indigo.platform.renderer.Renderer
import indigo.shared.AnimationsRegister
import indigo.shared.BoundaryLocator
import indigo.shared.FontRegister
Expand Down Expand Up @@ -281,7 +282,8 @@ class InputFieldTests extends munit.FunSuite {
Dice.loaded(1),
new InputState(Mouse.default, new Keyboard(keysUp, Batch.empty, None), Gamepad.default, Pointers.default),
new BoundaryLocator(new AnimationsRegister, new FontRegister, new DynamicText),
()
(),
Renderer.blackHole.captureScreen
)

object Samples {
Expand Down
2 changes: 1 addition & 1 deletion indigo/indigo/src/main/scala/indigo/BootResult.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import indigo.shared.shader.Shader
import indigo.shared.subsystems.SubSystem

/** The game bootstrapping process results in a `BootResult`, which only occurs once on initial game load. The boot
* result decribes all of the initial values of your game such as it's configuration, data, animations, assets, fonts,
* result describes all of the initial values of your game such as it's configuration, data, animations, assets, fonts,
* subsystems, and shaders. You can add additional assets, animations, fonts, and shaders later during the setup
* process, so it is recommended that you only load the bare minimum needed to get your game going during the boot
* phase.
Expand Down
14 changes: 7 additions & 7 deletions indigo/indigo/src/main/scala/indigo/IndigoGame.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ trait IndigoGame[BootData, StartUpData, Model, ViewModel] extends GameLauncher[S
def eventFilters: EventFilters

/** `boot` provides the initial boot up function for your game, accepting commandline-like arguments and allowing you
* to declare pre-requist assets assets and data that must be in place for your game to get going.
* to declare pre-request assets assets and data that must be in place for your game to get going.
*
* @param flags
* A simply key-value object/map passed in during initial boot.
* @return
* Bootup data consisting of a custom data type, animations, subsytems, assets, fonts, and the game's config.
* Bootup data consisting of a custom data type, animations, subsystems, assets, fonts, and the game's config.
*/
def boot(flags: Map[String, String]): Outcome[BootResult[BootData, Model]]

Expand All @@ -70,7 +70,7 @@ trait IndigoGame[BootData, StartUpData, Model, ViewModel] extends GameLauncher[S
* @param assetCollection
* Access to the Asset collection in order to, for example, parse text files.
* @param dice
* Psuedorandom number generator
* Pseudorandom number generator
* @return
* Return start up data, which can include animations and fonts that could not be declared at boot time.
*/
Expand Down Expand Up @@ -102,7 +102,7 @@ trait IndigoGame[BootData, StartUpData, Model, ViewModel] extends GameLauncher[S
* @param model
* The latest version of the model to read from.
* @return
* A function that maps GlobalEvent's to the next version of your model, and encapsuates failures or resulting
* A function that maps GlobalEvent's to the next version of your model, and encapsulates failures or resulting
* events within the Outcome wrapper.
*/
def updateModel(context: FrameContext[StartUpData], model: Model): GlobalEvent => Outcome[Model]
Expand All @@ -118,8 +118,8 @@ trait IndigoGame[BootData, StartUpData, Model, ViewModel] extends GameLauncher[S
* @param viewModel
* The latest version of the view model to read from.
* @return
* A function that maps GlobalEvent's to the next version of your view model, and encapsuates failures or resulting
* events within the Outcome wrapper.
* A function that maps GlobalEvent's to the next version of your view model, and encapsulates failures or
* resulting events within the Outcome wrapper.
*/
def updateViewModel(
context: FrameContext[StartUpData],
Expand All @@ -138,7 +138,7 @@ trait IndigoGame[BootData, StartUpData, Model, ViewModel] extends GameLauncher[S
* @param viewModel
* The latest version of the view model to read from.
* @return
* A function that produces a description of what to present next, and encapsuates failures or resulting events
* A function that produces a description of what to present next, and encapsulates failures or resulting events
* within the Outcome wrapper.
*/
def present(context: FrameContext[StartUpData], model: Model, viewModel: ViewModel): Outcome[SceneUpdateFragment]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package indigo.entry

import indigo.gameengine.FrameProcessor
import indigo.platform.renderer.Renderer
import indigo.scenes.SceneManager
import indigo.shared.BoundaryLocator
import indigo.shared.FrameContext
import indigo.shared.Outcome
import indigo.shared.collections.Batch
import indigo.shared.datatypes.BindingKey
import indigo.shared.datatypes.Rectangle
import indigo.shared.dice.Dice
import indigo.shared.events.EventFilters
import indigo.shared.events.GlobalEvent
Expand Down Expand Up @@ -33,10 +36,12 @@ final class ScenesFrameProcessor[StartUpData, Model, ViewModel](
globalEvents: Batch[GlobalEvent],
inputState: InputState,
dice: Dice,
boundaryLocator: BoundaryLocator
boundaryLocator: BoundaryLocator,
renderer: => Renderer
): Outcome[(Model, ViewModel, SceneUpdateFragment)] = {

val frameContext = new FrameContext[StartUpData](gameTime, dice, inputState, boundaryLocator, startUpData)
val frameContext =
new FrameContext[StartUpData](gameTime, dice, inputState, boundaryLocator, startUpData, renderer.captureScreen)

val processSceneViewModel: (Model, ViewModel) => Outcome[ViewModel] = (m, vm) =>
globalEvents
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package indigo.entry

import indigo.gameengine.FrameProcessor
import indigo.platform.renderer.Renderer
import indigo.shared.BoundaryLocator
import indigo.shared.FrameContext
import indigo.shared.Outcome
Expand Down Expand Up @@ -31,9 +32,11 @@ final class StandardFrameProcessor[StartUpData, Model, ViewModel](
globalEvents: Batch[GlobalEvent],
inputState: InputState,
dice: Dice,
boundaryLocator: BoundaryLocator
boundaryLocator: BoundaryLocator,
renderer: => Renderer
): Outcome[(Model, ViewModel, SceneUpdateFragment)] =
val frameContext = new FrameContext[StartUpData](gameTime, dice, inputState, boundaryLocator, startUpData)
val frameContext =
new FrameContext[StartUpData](gameTime, dice, inputState, boundaryLocator, startUpData, renderer.captureScreen)
Outcome.join(
for {
m <- processModel(frameContext, model, globalEvents)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package indigo.gameengine

import indigo.platform.renderer.Renderer
import indigo.shared.BoundaryLocator
import indigo.shared.Outcome
import indigo.shared.collections.Batch
Expand All @@ -18,5 +19,6 @@ trait FrameProcessor[StartUpData, Model, ViewModel]:
globalEvents: Batch[GlobalEvent],
inputState: InputState,
dice: Dice,
boundaryLocator: BoundaryLocator
boundaryLocator: BoundaryLocator,
renderer: => Renderer
): Outcome[(Model, ViewModel, SceneUpdateFragment)]
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ final class GameEngine[StartUpData, GameModel, ViewModel](
m,
vm,
frameProccessor,
!firstRun // If this isn't the first run, start with it frame locked.
!firstRun, // If this isn't the first run, start with it frame locked.
renderer
)
} yield {
renderer = rendererAndAssetMapping._1
Expand Down Expand Up @@ -363,7 +364,8 @@ object GameEngine {
initialModel: GameModel,
initialViewModel: GameModel => ViewModel,
frameProccessor: FrameProcessor[StartUpData, GameModel, ViewModel],
startFrameLocked: Boolean
startFrameLocked: Boolean,
renderer: => Renderer
): Outcome[GameLoop[StartUpData, GameModel, ViewModel]] =
Outcome(
new GameLoop[StartUpData, GameModel, ViewModel](
Expand All @@ -375,7 +377,8 @@ object GameEngine {
initialModel,
initialViewModel(initialModel),
frameProccessor,
startFrameLocked
startFrameLocked,
renderer
)
)

Expand Down
11 changes: 8 additions & 3 deletions indigo/indigo/src/main/scala/indigo/gameengine/GameLoop.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package indigo.gameengine

import indigo.platform.assets.AssetCollection
import indigo.platform.renderer.Renderer
import indigo.shared.BoundaryLocator
import indigo.shared.IndigoLogger
import indigo.shared.Outcome
Expand All @@ -12,6 +13,7 @@ import indigo.shared.events.GlobalEvent
import indigo.shared.events.IndigoSystemEvent
import indigo.shared.events.InputEvent
import indigo.shared.events.InputState
import indigo.shared.platform.ProcessedSceneData
import indigo.shared.platform.SceneProcessor
import indigo.shared.scenegraph.SceneUpdateFragment
import indigo.shared.time.GameTime
Expand All @@ -30,7 +32,8 @@ final class GameLoop[StartUpData, GameModel, ViewModel](
initialModel: GameModel,
initialViewModel: ViewModel,
frameProcessor: FrameProcessor[StartUpData, GameModel, ViewModel],
startFrameLocked: Boolean
startFrameLocked: Boolean,
renderer: => Renderer
):

@SuppressWarnings(Array("scalafix:DisableSyntax.var"))
Expand Down Expand Up @@ -129,7 +132,8 @@ final class GameLoop[StartUpData, GameModel, ViewModel](
events,
_inputState,
Dice.fromSeconds(gameTime.running),
boundaryLocator
boundaryLocator,
renderer
)

// Persist frame state
Expand Down Expand Up @@ -171,7 +175,8 @@ final class GameLoop[StartUpData, GameModel, ViewModel](
.foreach(systemActions.enqueue)

def performSystemActions(systemEvents: List[IndigoSystemEvent]): Unit =
systemEvents.foreach { case IndigoSystemEvent.Rebuild(assetCollection) =>
systemEvents.foreach { case IndigoSystemEvent.Rebuild(assetCollection, nextEvent) =>
IndigoLogger.info("Rebuilding game loop from new asset collection.")
rebuildGameLoop(assetCollection)
gameEngine.globalEventStream.pushGlobalEvent(nextEvent)
}
3 changes: 3 additions & 0 deletions indigo/indigo/src/main/scala/indigo/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,9 @@ val AnalogAxis: shared.input.AnalogAxis.type = shared.input.AnalogAxis
type GamepadButtons = shared.input.GamepadButtons
val GamepadButtons: shared.input.GamepadButtons.type = shared.input.GamepadButtons

type ImageType = shared.ImageType
val ImageType: shared.ImageType.type = shared.ImageType

type BoundaryLocator = shared.BoundaryLocator

type FrameContext[StartUpData] = shared.FrameContext[StartUpData]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@ object AssetLoader {
loadAssets(assets)
.onComplete {
case Success(ac) if makeAvailable =>
globalEventStream.pushGlobalEvent(AssetEvent.AssetBatchLoaded(key, true))
globalEventStream.pushGlobalEvent(IndigoSystemEvent.Rebuild(ac))
globalEventStream.pushGlobalEvent(
IndigoSystemEvent.Rebuild(ac, AssetEvent.AssetBatchLoaded(key, assets, true))
)

case Success(_) =>
globalEventStream.pushGlobalEvent(AssetEvent.AssetBatchLoaded(key, false))
globalEventStream.pushGlobalEvent(AssetEvent.AssetBatchLoaded(key, assets, false))

case Failure(e) =>
globalEventStream.pushGlobalEvent(AssetEvent.AssetBatchLoadError(key, e.getMessage()))
Expand Down
Loading
Loading