{"id":18985284,"url":"https://github.com/emptydreams/consolegameengine","last_synced_at":"2025-08-07T17:33:05.881Z","repository":{"id":151054837,"uuid":"621811346","full_name":"EmptyDreams/ConsoleGameEngine","owner":"EmptyDreams","description":"一个用于开发控制台小游戏的 JVM 端引擎。en: A console game engine for the JVM.","archived":false,"fork":false,"pushed_at":"2023-04-21T07:28:00.000Z","size":7225,"stargazers_count":9,"open_issues_count":0,"forks_count":3,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-19T21:21:37.746Z","etag":null,"topics":["c","jni","jvm","kotlin"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/EmptyDreams.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-31T12:44:11.000Z","updated_at":"2024-08-23T12:22:45.000Z","dependencies_parsed_at":null,"dependency_job_id":"981e6d3c-7b79-46d4-b28a-ebc932b65362","html_url":"https://github.com/EmptyDreams/ConsoleGameEngine","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/EmptyDreams/ConsoleGameEngine","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EmptyDreams%2FConsoleGameEngine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EmptyDreams%2FConsoleGameEngine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EmptyDreams%2FConsoleGameEngine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EmptyDreams%2FConsoleGameEngine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/EmptyDreams","download_url":"https://codeload.github.com/EmptyDreams/ConsoleGameEngine/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EmptyDreams%2FConsoleGameEngine/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":269295117,"owners_count":24393036,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-07T02:00:09.698Z","response_time":73,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["c","jni","jvm","kotlin"],"created_at":"2024-11-08T16:25:35.586Z","updated_at":"2025-08-07T17:33:05.834Z","avatar_url":"https://github.com/EmptyDreams.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"## 👋前言 \n\n　　该引擎设计用途是 JVM 端控制台小游戏开发，也可用于其它用途，目前实现了在控制台中显示“Bad Apple”的功能。\n\n[简体中文](README.md) | [English](README_en.md)\n\n✨ **Demo**\n\nhttps://user-images.githubusercontent.com/41804496/231933915-4928c5a4-e8c7-47f1-b411-90396c27a5e8.mp4  \n\n同步于： [Bilibili](https://www.bilibili.com/video/BV1kM411T7Bh) | [Youtube](https://www.youtube.com/watch?v=8MEbOSIptuA)\n\n🛠️目前引擎支持以下功能：\n\n- 任意位置填充字符串\n- 任意位置修改文本属性\n- 监听键盘和鼠标的按键\n- 监听鼠标坐标\n- 无闪屏刷新（多缓冲）\n\n⚠️注意：引擎目前只能在 **Windows** 平台使用，不支持其它平台！\n\n　　下面我们开始说明引擎的使用方法。  \n  \n## 🏗️引入仓库\n\n　　如果你使用`gradle`管理项目依赖，那么在`dependencies`语句中添加如下代码：\n\n```groovy\nimplementation group: 'top.kmar.game', name: 'cg-engine', version: 'x.x.x'\n```\n\n　　如果你使用`maven`管理项目依赖，请添加如下代码：\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003etop.kmar.game\u003c/groupId\u003e\n    \u003cartifactId\u003ecg-engine\u003c/artifactId\u003e\n    \u003cversion\u003ex.x.x\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n　　最后的`version`填写你想使用的版本，截止文档最后一次更新，最新版为`1.2.0`。\n\n　　如果你使用`sbt`、`ivy`、`grape`、`leiningen`或`buildr`管理依赖，请参考[Maven Central](https://central.sonatype.com/artifact/top.kmar.game/cg-engine/)。\n\n　　如果你不使用上面任意一种方法管理项目依赖，请克隆 Github 仓库并手动编译代码，然后添加到你的项目中。\n\n## ⚙️初始化和销毁 \n\n　　使用引擎之前，需要先初始化控制台的信息， `GMap.Builder` 中已经封装好了初始化的方法，调用 `GMap.Builder.build()` 时会自动初始化控制台信息。\n\n　　我们还可以使用 `ConsolePrinter.init` 进行手动初始化， 但是 **请注意** ：如果已经通过 `GMap.Builder` 进行了初始化操作，请勿通过 `ConsolePrinter.init` 重复初始化。\n\n　　如果游戏已经结束需要退出控制台，则需调用 `GMap.Builder.dispose()` 销毁控制台，正如注释中所说的一样，该函数只有在所有的 `GMap` 对象被虚拟机回收或调用 `close()` 函数后才会销毁控制台，这个设计是为了防止控制台被销毁后继续通过 `GMap` 操作控制台，从而导致程序崩溃。\n\n　　如果需要强制销毁，可以手动调用 `ConsolePrinter.dispose()` ，这个函数不会进行销毁检查，但是务必注意销毁控制台后不能再通过引擎对控制台进行操作。\n\n　　PS：控制台销毁后可以重新初始化。\n\n　　代码示例：\n\n🔔每个模块都将提供**Kotlin** / **Java** 两种语言的代码示例\n\n**Kotlin**\n\n```Kotlin\nfun main() {\n    GMap.Builder.apply {\n        width = 80\n        height = 40\n    }.build().use {\n        // do something\n        // 通过某种方法阻塞主线程直到程序结束\n    }\n    GMap.Builder.dispose()\n}\n```\n\n**Java**\n\n```Java\npublic class Main {\n\n    public static void main(String[] args) {\n        GMap.BuilderJava.INSTANCE\n                .setWidth(80)\n                .setHeight(40);\n        try (GMap map = GMap.Builder.build()) {\n            // do something\n            // 通过某种方法阻塞主线程直到程序结束\n        }\n        GMap.Builder.dispose();\n    }\n\n}\n```\n\n　　上述代码使用 `Builder` 创建了一个横向 80 个字符，纵向 40 个字符的地图， `BuilderJava` 是为 Java 语言特化的类，用于优化 Java 的调用体验，可以与 `Builder` 类混用。\n\n　　接下来我们介绍 `Builder` 中各个字段的作用（括号中为缺省值）：\n\n- `width` - 横向字符数量\n- `height` - 纵向字符数量\n- `fontWidth` - 字符宽度（10）\n- `cache` - 缓存数量（2）\n- `ignoreClose` - 是否忽略 `Ctrl + C` 一类的控制快捷键（false）\n- `file` - DLL 文件的路径（ `./utils.dll` ）\n\n　　由于控制台限制，字符宽度只能保证字符宽度与设置的值接近，并不能保证恒等于设置的值。理想的拉丁字符宽高比为 `1:2` ，中文字符宽高比为 `1:1` ，在有些时候会偏离这个比例，如果希望达到完美比例，需要调整 `fontWidth` 的值。\n\n## ⌨️键鼠监听 \n\n　　引擎支持监听键盘和鼠标的按键输入和鼠标坐标的变化，相关函数均封装在 `EventListener` 中，调用 `pushButtonEvent()` 函数即可发布一次按键事件，调用`pushMouseLocationEvent()`函数即可发布一次鼠标移动事件。\n\n　　可以通过 `registry...` 函数注册事件：\n\n- `registryKeyboardEvent` - 注册键盘事件\n- `registryMouseEvent` - 注册鼠标事件\n- `registryMousePosEvent` - 注册鼠标坐标事件\n\n　　上述函数均有对应的 `remove` 函数，可以用于移除指定事件。\n\n　　下面给出监听事件的示例代码：\n\n**Kotlin**\n\n```Kotlin\nfun main() {\n    EventListener.registryKeyboardEvent(object : IKeyboardListener {\n\n        override fun onPressed(code: Int) {\n            println(\"pressed: $code\")\n        }\n\n        override fun onReleased(code: Int) {\n            println(\"released: $code\")\n        }\n\n        override fun onActive(code: Int) {}\n\n    })\n    registryMousePosEvent { x, y, oldX, oldY -\u003e\n        println(\"mouse from ($oldX, $oldY) to ($x, $y)\")\n    }\n    while (true) {\n        EventListener.pushButtonEvent()\n        EventListener.pushMouseLocationEvent()\n        Thread.sleep(10)\n    }\n}\n```\n\n**Java**\n\n```Java\nimport java.util.EventListener;\n\npublic class Main {\n\n    public static void main(String[] args) {\n        EventListener.registryKeyboardEvent(new IKeyboardListener() {\n\n            @Override\n            public void onPressed(int code) {\n                System.out.println(\"pressed: \" + code);\n            }\n\n            @Override\n            public void onReleased(int code) {\n                System.out.println(\"released: \" + code);\n            }\n\n            @Override\n            public void onActive(int code) {\n            }\n\n        });\n        EventListener.registryMousePosEvent((x, y, oldX, oldY) -\u003e {\n            System.out.printf(\"mouse from (%d, %d) to (%d, %d)\\n\", oldX, oldY, x, y);\n        });\n        while (true) {\n            EventListener.pushButtonEvent();\n            EventListener.pushMouseLocationEvent();\n            try {\n                Thread.sleep(50);\n            } catch (InterruptedException ignored) { }\n        }\n    }\n\n}\n```\n\n　　请注意：调用 `pushButtonEvent` 的时候，引擎只能检测当前按下的按键，无法检测调用 `pushButtonEvent` 之前按下的按键，如果两次 `pushButtonEvent` 调用的时间间隔过大，会非常容易漏掉一些输入，所以应当适当提高调用频率。\n\n　　同理，调用 `pushMouseLocationEvent` 的时候也应当适当提高调用频率。\n\n## 💿自定义实体 \n\n　　引擎中所有内容都被设计为“实体”，实体有四个通用属性：\n\n- `visible` - 标记实体是否可见，为 `false` 时将跳过渲染\n- `collisible` - 标记实体是否具有碰撞箱，为 `false` 时将跳过碰撞箱检测\n- `died` - 标记实体是否死亡，为 `true` 时将自动从地图中移除\n- `x/y/width/height` - 坐标和尺寸信息\n\n　　实体（ `GEntity` ）中提供了一些事件：\n\n- `render` - 渲染时触发，用于渲染该实体，不可见实体不会触发该事件\n- `update` - 每次逻辑循环的更新事件\n- `beKilled` - 被其它实体“击杀”后触发\n- `onCollision` - 与其它实体碰撞时触发\n- `onRemove` - 实体被从地图中移除后触发\n\n　　实体中还需要实现一些工具函数：\n\n- `getCollision` - 获取当前实体指定区域内所有具有碰撞体积的点\n- `copy` - 深拷贝当前对象\n\n　　下面我们给出一个代码示例，在下面的代码中，我们实现了一个矩形可视可碰撞的实体（ `render` 函数中的代码后面会说明）：\n\n**Kotlin**\n\n```Kotlin\nclass BlockEntity(\n    override var x: Int,\n    override var y: Int,\n    override val width: Int,\n    override val height: Int\n) : GEntity {\n    \n    override val collisible = true\n    override var died = false\n        private set\n\n    override fun render(graphics: SafeGraphics) {\n        graphics.fillRect('#', 0, 0, width, height)\n    }\n\n    override fun getCollision(x: Int, y: Int, width: Int, height: Int): Stream\u003cPoint2D\u003e {\n        val builder = Stream.builder\u003cPoint2D\u003e()\n        val right = x + width\n        val bottom = y + height\n        for (i in y until bottom) {\n            for (k in x until right) {\n                builder.add(Point2D(k, i))\n            }\n        }\n        return builder.build()\n    }\n\n    override fun update(map: GMap, time: Long) {\n        // do something\n    }\n\n    override fun beKilled(map: GMap, killer: GEntity) {\n        died = true\n    }\n    \n    override fun copy() = BlockEntity(x, y, width, height)\n\n}\n```\n\n**Java**\n\n```Java\npublic class BlockEntity implements GEntity {\n\n    private boolean died = false;\n    private int x;\n    private int y;\n    private final int width;\n    private final int height;\n    \n    public BlockEntity(int x, int y, int width, int height) {\n        this.x = x;\n        this.y = y;\n        this.width = width;\n        this.height = height;\n    }\n\n    @Override\n    public void render(@NotNull SafeGraphics graphics) {\n        graphics.fillRect('#', 0, 0, width, height, -1);\n    }\n\n    @NotNull\n    @Override\n    public Stream\u003cPoint2D\u003e getCollision(int x, int y, int width, int height) {\n        Stream.Builder\u003cPoint2D\u003e builder = Stream.builder();\n        int right = x + width;\n        int bottom = y + height;\n        for (int i = y; y != bottom; ++y) {\n            for (int k = x; k != right; ++k) {\n                builder.add(new Point2D(k, i));\n            }\n        }\n        return builder.build();\n    }\n\n    @Override\n    public void update(@NotNull GMap map, long time) {\n        // do something\n    }\n\n    @Override\n    public void beKilled(@NotNull GMap map, @NotNull GEntity killer) {\n        died = true;\n    }\n\n    @Override\n    public boolean getCollisible() {\n        return true;\n    }\n\n    @Override\n    public boolean getDied() {\n        return died;\n    }\n\n    @Override\n    public int getX() {\n        return x;\n    }\n\n    @Override\n    public int getY() {\n        return y;\n    }\n\n    @Override\n    public int getWidth() {\n        return width;\n    }\n\n    @Override\n    public int getHeight() {\n        return height;\n    }\n\n    @NotNull\n    @Override\n    public GEntity copy() {\n        return new BlockEntity(x, y, width, height);\n    }\n    \n}\n```\n\n　　引擎还提供了一个抽象类`AbstractEntity`，其中实现了`x`、`y`等这些非常简单的属性，大部分实体都可以使用`AbstractEntity`来简化代码，上面的代码修改后为：\n\n**Kotlin**\n\n```kotlin\nclass BlockEntity(\n    x: Int, y: Int, width: Int, height: Int\n) : AbstractEntity(x, y, width, height, true, true) {\n\n    override fun render(graphics: SafeGraphics) {\n        graphics.fillRect('#', 0, 0, width, height)\n    }\n\n    override fun getCollision(x: Int, y: Int, width: Int, height: Int): Stream\u003cPoint2D\u003e {\n        val builder = Stream.builder\u003cPoint2D\u003e()\n        val right = x + width\n        val bottom = y + height\n        for (i in y until bottom) {\n            for (k in x until right) {\n                builder.add(Point2D(k, i))\n            }\n        }\n        return builder.build()\n    }\n\n    override fun update(map: GMap, time: Long) {\n        // do something\n    }\n\n    override fun copy() = BlockEntity(x, y, width, height)\n\n}\n```\n\n**Java**\n\n```java\npublic class BlockEntity extends AbstractEntity {\n    \n    public BlockEntity(int x, int y, int width, int height) {\n        super(x, y, width, height, true, true);\n    }\n\n    @Override\n    public void render(@NotNull SafeGraphics graphics) {\n        graphics.fillRect('#', 0, 0, width, height, -1);\n    }\n\n    @NotNull\n    @Override\n    public Stream\u003cPoint2D\u003e getCollision(int x, int y, int width, int height) {\n        Stream.Builder\u003cPoint2D\u003e builder = Stream.builder();\n        int right = x + width;\n        int bottom = y + height;\n        for (int i = y; y != bottom; ++y) {\n            for (int k = x; k != right; ++k) {\n                builder.add(new Point2D(k, i));\n            }\n        }\n        return builder.build();\n    }\n\n    @Override\n    public void update(@NotNull GMap map, long time) {\n        // do something\n    }\n\n    @NotNull\n    @Override\n    public GEntity copy() {\n        return new BlockEntity(x, y, width, height);\n    }\n\n}\n```\n\n## 🖊️使用画笔 \n\n　　引擎提供了 `SafeGraphics` 类用来绘制图形，其中封装了基础的绘制函数。同时正如其名所说的一样，这个类是一个“安全”的画笔类，它可以确保绘制的内容不会超过为其设定的边界。\n\n`GEntity#render` 函数会接收一个 `SafeGraphics` 的对象， `GMap` 会自动为其设定参数，在实体中使用时，该画笔的绘制坐标是相对于实体本身的，也就是说使用如下代码会在实体的左上角绘制一个 `#` ，而非是在地图左上角绘制：\n\n```\ngraphics.fillRect(#, 0, 0, 1, 1)\n```\n\n`SafeGraphics` 中很多函数都提供了缺省参数，不过由于 Java 不支持这个特性，所以如果使用 Java 编写代码，则需要手动填入所有参数。\n\n　　至于绘制中所用到的一些概念如下所示：\n\n- ATTR： 这是用于控制终端字体颜色、背景颜色等属性的值，所有支持的类型已在 `ConsolePrinter` 中列出。需要注意的是，在调用 `flush` 函数时， 同样不会清除上一次设置的 ATTR 信息。\n    除 clear 系列函数外的所有函数，传入 `attr = -1` 表示无效 attr，打印内容时将忽略 attr 信息。\n- 字符宽度： 在控制台中，不同字符宽度不同，拉丁文字符宽度为 1，而中文字符宽度为 2，计算字符宽度时，满足 `char \u003c 0x100` 的宽度视为 1，否则为 2。\n    更多的内容可以查阅 `ConsolePrinter` 类中的注释，其中有详细说明。\n\n　　一般情况下，我们 `GMap` 类会自动为用户生成画笔，并不需要用户自己创建，但是如果想要手动创建画笔对象的话可以参考以下代码：\n\n**Kotlin**\n\n```Kotlin\nfun main() {\n    GMap.Builder.apply {\n        width = 80\n        height = 40\n    }.build().use {\n        // 创建一个和地图等大的画笔\n        val g1 = SafeGraphics(it)\n        // 创建一个绘制起点为 (10, 5)，绘制区域大小为 20x10 的画笔，绘制区域受地图尺寸限制\n        val g2 = SafeGraphics(it, 10, 5, 20, 10)\n        // 创建一个绘制起点为 (10, 5)，绘制区域大小为 20x10 的画笔，绘制区域不受任何限制\n        val g3 = HalfSafeGraphics(10, 5, 20, 10, ConsolePrinter.index)\n        // do something\n    }\n    GMap.Builder.dispose()\n}\n```\n\n**Java**\n\n```Java\npublic class Main {\n\n    public static void main(String[] args) {\n        GMap.BuilderJava.INSTANCE\n                .setWidth(80)\n                .setHeight(40);\n        try (GMap map = GMap.Builder.build()) {\n            // 创建一个和地图等大的画笔\n            SafeGraphics g1 = SafeGraphics.createSafeGraphics(map);\n            // 创建一个绘制起点为 (10, 5)，绘制区域大小为 20x10 的画笔，绘制区域受地图尺寸限制\n            SafeGraphics g2 = SafeGraphics.createSafeGraphics(map, 10, 5, 20, 10);\n            // 创建一个绘制起点为 (10, 5)，绘制区域大小为 20x10 的画笔，绘制区域不受任何限制\n            SafeGraphics g3 = SafeGraphics.createHalfSafeGraphics(10, 5, 20, 10, ConsolePrinter.getIndex());\n            // do something\n        }\n        GMap.Builder.dispose();\n    }\n\n}\n```\n\n## ⏲️时序控制 \n\n　　引擎支持用户自己进行时序控制，也支持让引擎接管所有时序控制，调用 `GMap#start` 即可使引擎接管所有任务。\n\n　　**请注意！！！**\n\n　　引擎接管时序控制后，会创建三个线程，分别用于执行“逻辑任务”、“渲染任务”和“事件任务”，编写代码时请注意线程同步问题。`GMap` 中已经准备好了一个线程安全的接口——`appendTask`，这个接口用于在任意位置添加需要在指定位置执行的任务，目前支持五种任务：\n\n+ `BEFORE_UPDATE` - 在 `update` 执行前触发\n+ `AFTER_UPDATE` - 在 `update` 执行后触发\n+ `BEFORE_RENDER` - 在 `render` 执行前触发\n+ `AFTER_RENDER` - 在 `render` 执行后触发\n+ `AFTER_LOGIC` - 在整个逻辑循环执行完毕并确定继续下一次逻辑循环后调用（该任务**仅在使用内置的时序控制时有效**）\n\n　　其中，`update` 相关任务和 `AFTER_LOGIC` 会在逻辑线程执行，`render` 相关任务会在渲染线程执行。\n\n　　引擎还提供了一个类似的接口——`appendReusableTask`，与前者不同的是，通过该函数添加的任务在执行后不会被移除，其中的任务会持续生效，如果需要删除任务，可以使用 `removeReusableTask` 函数。\n\n　　示例代码：：\n\n**Kotlin**\n\n```Kotlin\nfun main() {\n    GMap.Builder.apply {\n        width = 80\n        height = 40\n    }.build().use { map -\u003e\n        EventListener.registryKeyboardEvent(object : IKeyboardListener {\n\n            override fun onPressed(code: Int) {\n                map.appendTask(GMap.BEFORE_UPDATE) {\n                    println(\"pressed: $code\")\n                }\n            }\n\n            override fun onReleased(code: Int) {\n                map.appendTask(GMap.BEFORE_UPDATE) {\n                    println(\"released: $code\")\n                }\n            }\n\n            override fun onActive(code: Int) { }\n\n        })\n        map.start(10, 50, 0) { true }\n    }\n    GMap.Builder.dispose()\n}\n```\n\n**Java**\n\n```Java\npublic class Main {\n\n    public static void main(String[] args) {\n        GMap.BuilderJava.INSTANCE\n                .setWidth(80)\n                .setHeight(40);\n        try (GMap map = GMap.Builder.build()) {\n            EventListener.registryKeyboardEvent(new IKeyboardListener() {\n\n                @Override\n                public void onPressed(int code) {\n                    map.appendTask(GMap.BEFORE_UPDATE, () -\u003e System.out.println(\"pressed: $code\"));\n                }\n\n                @Override\n                public void onReleased(int code) {\n                    map.appendTask(GMap.BEFORE_UPDATE, () -\u003e System.out.println(\"released: $code\"));\n                }\n\n                @Override\n                public void onActive(int code) {\n                }\n\n            });\n            map.start(10, 50, 0, () -\u003e true);\n        }\n        GMap.Builder.dispose();\n    }\n\n}\n```\n\n　　以上就是引擎的基本用法，代码注释中已经尽量详细的描述代码的用法和注意事项，如果存有疑问欢迎提 issue。","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femptydreams%2Fconsolegameengine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Femptydreams%2Fconsolegameengine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Femptydreams%2Fconsolegameengine/lists"}