2 Commits

79 changed files with 3527 additions and 12958 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.6 MiB

View File

@@ -162,3 +162,42 @@ Thumbs.db
## You could also add that configuration to the text in nativeimage.gradle .
## You should delete or comment out the next line if you have configuration in a different resource-config.json .
**/resource-config.json
# Created by https://www.toptal.com/developers/gitignore/api/c++
# Edit at https://www.toptal.com/developers/gitignore?templates=c++
### C++ ###
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
GameServer/server
# End of https://www.toptal.com/developers/gitignore/api/c++

View File

@@ -0,0 +1,256 @@
#include <SFML/Network.hpp>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
// TODO: move `GameServer` into its own files (h/cpp).
// Note: This is compiled with SFML 2.6.2 in mind.
// It would work similarly with slightly older versions of SFML.
// A thourough rework is necessary for SFML 3.0.
enum GameMessageType : unsigned char {
JOIN_GAME = 0x01, PLACE_TOKEN = 0x02, START_GAME = 0x03, GAME_OVER = 0x04
};
enum Token : unsigned char {
NOUGHTS = 0x01, CROSSES = 0x02
};
class GameServer {
public:
GameServer(unsigned short tcp_port) :
m_tcp_port(tcp_port) {}
bool send_start_game_to_clients() {
char buf[1] = { START_GAME };
std::cout << "Starting the game..." << std::endl;
return broadcast_message(buf, nullptr);
}
bool send_game_over_to_clients() {
char buf[1] = { GAME_OVER };
std::cout << "Game Over!" << std::endl;
return broadcast_message(buf, nullptr);
}
// Binds to a port and then loops around. For every client that connects,
// we start a new thread receiving their messages.
void tcp_start()
{
// BINDING
sf::TcpListener listener;
sf::Socket::Status status = listener.listen(m_tcp_port, sf::IpAddress("152.105.66.120")); // Make sure to change this!
if (status != sf::Socket::Status::Done)
{
std::cerr << "Error binding listener to port" << std::endl;
return;
}
std::cout << "TCP Server is listening to port "
<< m_tcp_port
<< ", waiting for connections..."
<< std::endl;
while (true)
{
// ACCEPTING
if(m_player_count < 2)
{
sf::TcpSocket* client = new sf::TcpSocket;
status = listener.accept(*client);
if (status != sf::Socket::Status::Done)
{
delete client;
} else {
{
std::lock_guard<std::mutex> lock(m_clients_mutex);
m_clients.push_back(client);
}
std::cout << "New client connected: "
<< client->getRemoteAddress()
<< std::endl;
m_player_count++;
std::thread(&GameServer::handle_client, this, client, m_player_count).detach();
if(m_player_count == 2)
{
// --------------------------------------------------------------
// Slight pause to ensure the all threads have started
// --------------------------------------------------------------
std::this_thread::sleep_for(std::chrono::milliseconds(250));
if(!send_start_game_to_clients()) {
std::cerr << "Could not start game. One or both players did not recieve START_GAME" << std::endl;
}
}
}
}
}
// No need to call close of the listener.
// The connection is closed automatically when the listener object is out of scope.
}
private:
unsigned short m_tcp_port;
unsigned short m_player_count { 0 };
unsigned short m_turns_played { 0 };
int board[3][3];
std::vector<sf::TcpSocket*> m_clients;
std::mutex m_clients_mutex;
// Loop around, receive messages from client and send them to all
// the other connected clients.
void handle_client(sf::TcpSocket* client, unsigned short player_num)
{
sf::Socket::Status status;
if(player_num == 1) {
std::cout << "Player " << player_num << " is NOUGHTS" << std::endl;
char buffer[2] = {
GameMessageType::JOIN_GAME,
Token::NOUGHTS
};
status = client->send(buffer, message_size(GameMessageType::JOIN_GAME));
if (status != sf::Socket::Status::Done)
{
std::cerr << "Error sending JOIN_GAME to player 1" << std::endl;
return;
}
}
else if(player_num == 2) {
std::cout << "Player " << player_num << " is CROSSES" << std::endl;
char buffer[2] = {
GameMessageType::JOIN_GAME,
Token::CROSSES
};
status = client->send(buffer, message_size(GameMessageType::JOIN_GAME));
if (status != sf::Socket::Status::Done)
{
std::cerr << "Error sending JOIN_GAME to player 2" << std::endl;
return;
}
}
else {
return; // No more players please!!!
}
while (true)
{
// RECEIVING
char payload[1024];
std::memset(payload, 0, 1024);
size_t received;
sf::Socket::Status status = client->receive(payload, 1024, received);
if (status != sf::Socket::Status::Done)
{
std::cerr << "Error receiving message from client" << std::endl;
break;
} else {
// Actually, there is no need to print the message if the message is not a string
debug_message(payload);
broadcast_message(payload, client);
std::this_thread::sleep_for(std::chrono::milliseconds(250));
if(++m_turns_played == 9) {
send_game_over_to_clients();
}
}
}
// Everything that follows only makes sense if we have a graceful way to exiting the loop.
// Remove the client from the list when done
{
std::lock_guard<std::mutex> lock(m_clients_mutex);
m_clients.erase(std::remove(m_clients.begin(), m_clients.end(), client),
m_clients.end());
}
delete client;
}
// Sends `message` from `sender` to all the other connected clients
bool broadcast_message(const char *buffer, sf::TcpSocket* sender)
{
size_t msgSize { message_size(buffer[0]) };
// You might want to validate the message before you send it.
// A few reasons for that:
// 1. Make sure the message makes sense in the game.
// 2. Make sure the sender is not cheating.
// 3. First need to synchronise the players inputs (usually done in Lockstep).
// 4. Compensate for latency and perform rollbacks (usually done in Ded Reckoning).
// 5. Delay the sending of messages to make the game fairer wrt high ping players.
// This is where you can write the authoritative part of the server.
std::lock_guard<std::mutex> lock(m_clients_mutex);
for (auto& client : m_clients)
{
if (client != sender)
{
// SENDING
sf::Socket::Status status = client->send(buffer, msgSize) ;
if (status != sf::Socket::Status::Done)
{
std::cerr << "Error sending message to client" << std::endl;
return false;
}
}
}
return true;
}
constexpr size_t message_size(const char messageType)
{
switch(messageType) {
case JOIN_GAME: return 2;
case PLACE_TOKEN: return sizeof(int) * 2 + 2;
case START_GAME: return 1;
case GAME_OVER: return 1;
default: return 0;
}
}
void debug_message(const char *buf)
{
const unsigned char msgType = buf[0];
switch(msgType) {
case JOIN_GAME: {
std::cout << "Player Joined The Game" << std::endl;
break;
}
case PLACE_TOKEN: {
const unsigned char *row { (unsigned char* )buf + 1 };
const unsigned char *col { (unsigned char* )buf + 1 + sizeof(int) };
unsigned int rowI = be32toh(*((unsigned int *) row));
unsigned int colI = be32toh(*((unsigned int *) col));
std::cout << "Player Placed A Token: (" << rowI << ", " << colI << ")" << std::endl;
break;
}
}
}
};
int main()
{
GameServer server(4331);
server.tcp_start();
return 0;
}

View File

@@ -0,0 +1,16 @@
SFML_PATH=/usr/local/Cellar/sfml/2.6.1/
CXXFLAGS= -std=c++14 -Wall -Wpedantic -I${SFML_PATH}include/
LDFLAGS=-L${SFML_PATH}lib/
CFLAGS=-g -lsfml-graphics -lsfml-window -lsfml-system -lsfml-network -pthread
CPPFLAGS=
LDLIBS=
LIBS=
CPP=g++
all: server
server: GameServer.o
$(CPP) $(CXXFLAGS) $(LDFLAGS) $(LIBS) $^ -o $@ $(CFLAGS)
clean:
\rm -f *.o server

View File

@@ -1,4 +1,4 @@
# uitest
# NetworkedTicTacToe
A [libGDX](https://libgdx.com/) project generated with [gdx-liftoff](https://github.com/libgdx/gdx-liftoff).
@@ -7,6 +7,7 @@ This project was generated with a Kotlin project template that includes Kotlin a
## Platforms
- `core`: Main module with the application logic shared by all platforms.
- `lwjgl3`: Primary desktop platform using LWJGL3; was called 'desktop' in older docs.
- `android`: Android mobile platform. Needs Android SDK.
## Gradle
@@ -26,6 +27,8 @@ Useful Gradle tasks and flags:
- `clean`: removes `build` folders, which store compiled classes and built archives.
- `eclipse`: generates Eclipse project data.
- `idea`: generates IntelliJ project data.
- `lwjgl3:jar`: builds application's runnable jar, which can be found at `lwjgl3/build/libs`.
- `lwjgl3:run`: starts the application.
- `test`: runs unit tests (if any).
Note that most tasks that are not specific to a single project can be run with `name:` prefix, where the `name` should be replaced with the ID of a specific project.

View File

@@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:glEsVersion="0x00020000" android:required="true"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:fullBackupContent="true"
@@ -12,7 +14,7 @@
tools:ignore="UnusedAttribute"
android:theme="@style/GdxTheme">
<activity
android:name="com.iofferyoutea.uitest.android.AndroidLauncher"
android:name="u0012604.tictactoe.android.AndroidLauncher"
android:label="@string/app_name"
android:screenOrientation="landscape"
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|screenLayout"

View File

@@ -9,7 +9,7 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace = "com.iofferyoutea.uitest"
namespace = "u0012604.tictactoe"
compileSdk = 35
sourceSets {
main {
@@ -31,7 +31,7 @@ android {
}
}
defaultConfig {
applicationId 'com.iofferyoutea.uitest'
applicationId 'u0012604.tictactoe'
minSdkVersion 21
targetSdkVersion 35
versionCode 1
@@ -130,7 +130,7 @@ tasks.register('run', Exec) {
}
def adb = path + "/platform-tools/adb"
commandLine "$adb", 'shell', 'am', 'start', '-n', 'com.iofferyoutea.uitest/com.iofferyoutea.uitest.android.AndroidLauncher'
commandLine "$adb", 'shell', 'am', 'start', '-n', 'u0012604.tictactoe/u0012604.tictactoe.android.AndroidLauncher'
}
eclipse.project.name = appName + "-android"

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">uitest</string>
<string name="app_name">NetworkedTicTacToe</string>
</resources>

View File

@@ -1,10 +1,10 @@
package com.iofferyoutea.uitest.android
package u0012604.tictactoe.android
import android.os.Bundle
import com.badlogic.gdx.backends.android.AndroidApplication
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration
import com.iofferyoutea.uitest.Main
import u0012604.tictactoe.Main
/** Launches the Android application. */
class AndroidLauncher : AndroidApplication() {

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -7,7 +7,7 @@ buildscript {
maven { url = 'https://central.sonatype.com/repository/maven-snapshots/' }
}
dependencies {
classpath "com.android.tools.build:gradle:8.9.3"
classpath 'com.android.tools.build:gradle:8.13.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
@@ -61,7 +61,7 @@ configure(subprojects - project(':android')) {
subprojects {
version = "$projectVersion"
ext.appName = 'uitest'
ext.appName = 'NetworkedTicTacToe'
repositories {
mavenCentral()
// You may want to remove the following line if you have errors downloading dependencies.
@@ -71,4 +71,4 @@ subprojects {
}
}
eclipse.project.name = 'uitest' + '-parent'
eclipse.project.name = 'NetworkedTicTacToe' + '-parent'

View File

@@ -7,6 +7,7 @@ dependencies {
api "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
api "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
api "com.badlogicgames.gdx:gdx:$gdxVersion"
api "com.kotcrab.vis:vis-ui:$visUiVersion"
api "io.github.libktx:ktx-actors:$ktxVersion"
api "io.github.libktx:ktx-ai:$ktxVersion"
api "io.github.libktx:ktx-app:$ktxVersion"
@@ -40,7 +41,3 @@ dependencies {
implementation "io.github.berstanio:gdx-svmhelper-annotations:$graalHelperVersion"
}
}
jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

View File

@@ -0,0 +1,110 @@
package u0012604.tictactoe
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.g2d.Batch
import com.badlogic.gdx.graphics.glutils.ShapeRenderer
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.utils.Disposable
import com.badlogic.gdx.utils.viewport.Viewport
import ktx.assets.disposeSafely
import ktx.graphics.use
import u0012604.tictactoe.screens.FirstScreen.Companion.NOUGHT_RADIUS
class Board(val viewport: Viewport) : Disposable {
private var thirdOfWidth = 0f
private var thirdOfHeight = 0f
private var halfCellW = 0f
private var halfCellH = 0f
private val shapeRenderer = ShapeRenderer()
private var boardLines = emptyArray<Pair<Vector2, Vector2>>()
private val board = arrayOf(
arrayOf(Token.EMPTY, Token.EMPTY, Token.EMPTY),
arrayOf(Token.EMPTY, Token.EMPTY, Token.EMPTY),
arrayOf(Token.EMPTY, Token.EMPTY, Token.EMPTY)
)
fun placeToken(row: Int, col: Int, token: Token) =
if(row in (0..2) && col in (0 .. 2) && board[row][col] == Token.EMPTY) {
board[row][col] = token
true
}
else {
false
}
fun draw(batch: Batch) {
shapeRenderer.use(ShapeRenderer.ShapeType.Filled, viewport.camera.combined) { sr ->
sr.color = Color.RED
boardLines.forEach { line ->
// Gdx.app.log(TAG, "p0:${line.first}, ${line.second}")
sr.rectLine(line.first, line.second, 10f)
}
board.forEachIndexed { rowIndex, row ->
row.forEachIndexed { colIndex, col ->
when(col) {
Token.NOUGHT -> drawNought(rowIndex, colIndex, sr)
Token.CROSS -> drawCross(rowIndex, colIndex, sr)
else -> {}
}
}
}
}
}
fun resize(width: Int, height: Int) {
boardLines = emptyArray()
thirdOfWidth = width / 3f
thirdOfHeight = height / 3f
// Vertical lines
val x1 = thirdOfWidth
val x2 = Gdx.graphics.width / 1.5f
boardLines += Pair(Vector2(x1, 0f), Vector2(x1, height.toFloat()))
boardLines += Pair(Vector2(x2, 0f), Vector2(x2, height.toFloat()))
// Horizontal lines
val y1 = thirdOfHeight
val v2 = Gdx.graphics.height.toFloat() / 1.5f
boardLines += Pair(Vector2(0f, y1), Vector2(width.toFloat(), y1))
boardLines += Pair(Vector2(0f, v2), Vector2(width.toFloat(), v2))
halfCellW = x1 / 2f
halfCellH = y1 / 2f
}
private fun drawNought(row: Int, col: Int, sr: ShapeRenderer) {
val x = col * thirdOfWidth + halfCellW
val y = (2 - row) * thirdOfHeight + halfCellH
sr.circle(x, y, NOUGHT_RADIUS)
}
private fun drawCross(row: Int, col: Int, sr: ShapeRenderer) {
val flipRow = 2 - row
val l1x1 = col * thirdOfWidth + 50f
val l1y1 = flipRow * thirdOfHeight + 50f
val l1x2 = col * thirdOfWidth - 50f + 2 * halfCellW
val l1y2 = flipRow * thirdOfHeight - 50f + 2 * halfCellH
sr.rectLine(l1x1, l1y1, l1x2, l1y2, 10f)
sr.rectLine(l1x1, l1y2, l1x2, l1y1, 10f)
}
override fun dispose() {
shapeRenderer.disposeSafely()
}
}

View File

@@ -0,0 +1,70 @@
package u0012604.tictactoe
import java.nio.ByteBuffer
interface Serializable {
fun serialize() : ByteArray
}
interface Deserializable {
fun deserialize(bb: ByteBuffer) : GameMessage
}
enum class GameMessageType(val id: Byte) {
JOIN_GAME(1),
PLACE_TOKEN(2),
START_GAME(3),
GAME_OVER(4);
companion object {
fun fromByte(id: Byte) = entries.first { it.id == id }
}
}
sealed class GameMessage(val type: GameMessageType) : Serializable {
override fun serialize() = byteArrayOf(type.id)
object StartGameMessage : GameMessage(GameMessageType.START_GAME)
object GameOverMessage : GameMessage(GameMessageType.GAME_OVER)
// -----------------------------------------------------------------------------------------------
data class JoinGameMessage(val token: Token) : GameMessage(GameMessageType.JOIN_GAME) {
override fun serialize(): ByteArray = with(ByteBuffer.allocate(2)) {
put(token.type)
super.serialize() + array()
}
companion object : Deserializable {
override fun deserialize(bb: ByteBuffer) = with(bb) {
JoinGameMessage(Token.fromByte(get()))
}
}
}
// -----------------------------------------------------------------------------------------------
data class PlaceTokenMessage(val row: Int, val col: Int, val token: Token) : GameMessage(GameMessageType.PLACE_TOKEN) {
override fun serialize(): ByteArray = with(ByteBuffer.allocate(10)) {
putInt(row)
putInt(col)
put(token.type)
super.serialize() + array()
}
companion object : Deserializable {
override fun deserialize(bb: ByteBuffer) = with(bb) {
val row = getInt()
val col = getInt()
val token = Token.fromByte(get())
PlaceTokenMessage(row, col, token)
}
}
}
}

View File

@@ -0,0 +1,40 @@
package u0012604.tictactoe
import kotlinx.coroutines.channels.Channel
import ktx.app.KtxGame
import ktx.app.KtxScreen
import ktx.assets.disposeSafely
import ktx.async.KtxAsync
import u0012604.tictactoe.networking.NetworkHandler
import u0012604.tictactoe.screens.FirstScreen
import u0012604.tictactoe.screens.GameOverScreen
class Main : KtxGame<KtxScreen>() {
private val clientChannel = Channel<GameMessage>(10)
private val serverChannel = Channel<GameMessage>(10)
private lateinit var networkHandler: NetworkHandler
override fun create() {
KtxAsync.initiate()
networkHandler = NetworkHandler("152.105.66.120", 4331, serverChannel, clientChannel)
addScreen(FirstScreen(this, clientChannel, serverChannel))
addScreen(GameOverScreen(0))
setScreen<FirstScreen>()
}
override fun dispose() {
super.dispose()
serverChannel.close()
clientChannel.close()
networkHandler.disposeSafely()
}
}

View File

@@ -0,0 +1,9 @@
package u0012604.tictactoe
enum class Token(val type: Byte) {
EMPTY(0), NOUGHT(1), CROSS(2);
companion object {
fun fromByte(type: Byte) = Token.entries.first { it.type == type }
}
}

View File

@@ -0,0 +1,170 @@
package u0012604.tictactoe.networking
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.utils.Disposable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import u0012604.tictactoe.GameMessage
import u0012604.tictactoe.GameMessageType
import java.net.ConnectException
import java.net.InetSocketAddress
import java.net.Socket
import java.net.SocketException
import java.net.SocketTimeoutException
import java.nio.ByteBuffer
class NetworkHandler(
private val host: String,
private val port: Int,
private val toServerChannel: Channel<GameMessage>, // Messages being received elsewhere in the client to send onwards to the client
private val fromServerChannel: SendChannel<GameMessage>, // Messages being sent from the server to elsewhere within client
private val connectionTimeout: Int = 3000,
private val maxRetries: Int = 10
) : Disposable
{
fun ByteArray.processMessage() = decodeToString().trimEnd{it == Char(0)} // Removes trailing null character
private val coroutineScope: CoroutineScope =
CoroutineScope(SupervisorJob() + Dispatchers.IO).apply {
launch {
var retries = 0
while(retries < maxRetries) {
if(startNetwork(host, port)) {
break;
}
delay(retries * 500L)
retries++
}
if(retries == maxRetries) {
Gdx.app.error(TAG, "Maximum retries ($maxRetries) exceeded, giving up :(")
}
}
}
private var socket: Socket? = null
val isReady: Boolean
get() = socket?.isConnected ?: false
@OptIn(ExperimentalStdlibApi::class)
private fun startNetwork(
host: String,
port: Int
) = try {
var assignedClientId: UShort = 0U
// Create our socket
socket = Socket()
// Connect with timeout set.
socket?.let {
val socketAddress = InetSocketAddress(host, port)
it.connect(socketAddress, connectionTimeout)
// -------------------------------------------------------------
// Coroutine to handle messages
// to be sent to the server
// -------------------------------------------------------------
coroutineScope.launch {
try {
it.outputStream.apply {
while (true) {
// 1. Get the next message in the channel
// to send onward to the server.
val nextMessage = toServerChannel.receive()
// 2. Write the message to the server
// via the socket's output stream
write(nextMessage.serialize())
flush()
Gdx.app.log(TAG, "Sent the message $nextMessage")
delay(10L)
}
}
} catch (ex: java.net.SocketException) {
Gdx.app.error(TAG, "[SEND] Socket Failed: ${ex.message}")
}
}
// -------------------------------------------------------------
// Coroutine to handle messages
// being received from the server
//
// Messages received are then forwarded via the
// -------------------------------------------------------------
coroutineScope.launch {
try {
it.inputStream.apply {
launch {
while (true) {
val byteArray = ByteArray(1024)
// delay(250L)
// 1. Read data from the socket's
// input stream.
val count = read(byteArray, 0, 1024)
if (count == -1) {
Gdx.app.error(TAG, "Socket Read Error!")
break
}
val gameMessage = buildGameMessage(byteArray)
fromServerChannel.send(gameMessage)
}
}
}
} catch (ex: java.net.SocketException) {
Gdx.app.log(TAG, "[RECEIVE] Socket Failure::[${ex.message}")
}
}
}
true
} catch (ex: java.net.SocketTimeoutException) {
Gdx.app.error(TAG, "Timeout Exception: ${ex.message}")
false
} catch (ex: java.net.ConnectException) {
Gdx.app.error(TAG, "Connection Exception: ${ex.message}")
false
} catch (ex: java.net.SocketException) {
Gdx.app.error(TAG, "Exception Raised: ${ex.message}")
false
}
fun buildGameMessage(ba: ByteArray) = with(ByteBuffer.wrap(ba)) {
val messageType = GameMessageType.fromByte(get())
when(messageType) {
GameMessageType.JOIN_GAME -> GameMessage.JoinGameMessage.deserialize(this)
GameMessageType.PLACE_TOKEN -> GameMessage.PlaceTokenMessage.deserialize(this)
GameMessageType.START_GAME -> GameMessage.StartGameMessage
GameMessageType.GAME_OVER -> GameMessage.GameOverMessage
}
}
override fun dispose() {
coroutineScope.cancel()
}
companion object {
val TAG = NetworkHandler::class.simpleName!!
}
}

View File

@@ -0,0 +1,198 @@
package u0012604.tictactoe.screens
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.InputProcessor
import com.badlogic.gdx.graphics.OrthographicCamera
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.utils.viewport.FitViewport
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ktx.app.KtxGame
import ktx.app.KtxScreen
import ktx.app.clearScreen
import ktx.assets.disposeSafely
import ktx.graphics.use
import u0012604.tictactoe.Board
import u0012604.tictactoe.GameMessage
import u0012604.tictactoe.Token
class FirstScreen(
val game: KtxGame<KtxScreen>,
private val receiveChannel: ReceiveChannel<GameMessage>,
private val sendChannel: SendChannel<GameMessage>
) : KtxScreen, InputProcessor {
private var gameStarted = false
private var gameOver = false
private val batch = SpriteBatch()
private val camera =
OrthographicCamera(WIDTH, HEIGHT).apply {
position.set(Gdx.graphics.width / 2f, Gdx.graphics.height / 2f, 0f)
};
private val viewport = FitViewport(Gdx.graphics.width.toFloat(), Gdx.graphics.height.toFloat(), camera)
private val board = Board(viewport)
private var touchPosition = Vector2.Zero
private var localPlayerToken: Token? = null
private var localPlayerTurn = false
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
init {
Gdx.input.inputProcessor = this
initiateIncomingGameMessageHandling()
}
override fun render(delta: Float) {
clearScreen(red = 0.7f, green = 0.7f, blue = 0.7f)
if (gameOver) {
game.setScreen<GameOverScreen>()
disposeSafely()
return
}
camera.update()
batch.use { board.draw(it) }
}
override fun resize(width: Int, height: Int) {
viewport.update(width, height)
board.resize(width, height)
}
private fun initiateIncomingGameMessageHandling() {
coroutineScope.launch {
while(true) {
val gm = receiveChannel.receive()
when(gm) {
is GameMessage.JoinGameMessage -> {
localPlayerToken = gm.token
localPlayerTurn = localPlayerToken == Token.NOUGHT
}
is GameMessage.PlaceTokenMessage -> {
Gdx.app.log(TAG, "The other player placed a token at: (${gm.row}, ${gm.col})")
board.placeToken(gm.row, gm.col, gm.token)
localPlayerTurn = true
}
is GameMessage.StartGameMessage -> {
Gdx.app.log(TAG, "The Server has started the game")
gameStarted = true
}
is GameMessage.GameOverMessage -> {
Gdx.app.log(TAG, "The Server has sent Game Over")
gameStarted = false
gameOver = true
}
}
Gdx.app.log(TAG, "THE MESSAGE RECEIVED IS: $gm")
delay(10L)
}
}
}
override fun dispose() {
batch.disposeSafely()
board.disposeSafely()
}
override fun keyDown(keycode: Int) = true
override fun keyUp(keycode: Int) = true
override fun keyTyped(character: Char) = true
override fun touchDown(
screenX: Int,
screenY: Int,
pointer: Int,
button: Int
): Boolean {
if(!gameStarted || !localPlayerTurn)
return true;
val col = ((screenX.toFloat() / Gdx.graphics.width) * 3).toInt()
val row = ((screenY.toFloat() / Gdx.graphics.height) * 3).toInt()
localPlayerToken?.let {
if (board.placeToken(row, col, it)) {
localPlayerTurn = false
val gameMessage = GameMessage.PlaceTokenMessage(row, col, it)
coroutineScope.launch {
sendChannel.send(gameMessage)
}
}
}
Gdx.app.log(TAG, "THE ROW IS: ($row, $col)")
touchPosition.set(screenX.toFloat(), screenY.toFloat())
viewport.unproject(touchPosition)
return true
}
override fun touchUp(
screenX: Int,
screenY: Int,
pointer: Int,
button: Int
) = true
override fun touchCancelled(
screenX: Int,
screenY: Int,
pointer: Int,
button: Int
) = true
override fun touchDragged(
screenX: Int,
screenY: Int,
pointer: Int
) = true
override fun mouseMoved(screenX: Int, screenY: Int) = true
override fun scrolled(amountX: Float, amountY: Float) = true
companion object {
val TAG = FirstScreen::class.simpleName!!
const val WIDTH = 100f
const val HEIGHT = 16f * WIDTH / 9f
const val NOUGHT_RADIUS = 100f
}
}

View File

@@ -0,0 +1,30 @@
package u0012604.tictactoe.screens
import com.badlogic.gdx.graphics.g2d.BitmapFont
import com.badlogic.gdx.graphics.g2d.SpriteBatch
import ktx.app.KtxScreen
import ktx.app.clearScreen
import ktx.graphics.use
class GameOverScreen(private val winType: Int) : KtxScreen {
private var label = winType.toString()
private val font = BitmapFont()
private val batch = SpriteBatch()
override fun show() {
when (winType) {
0 -> label = "You Win!"
1 -> label = "You Lose."
2 -> label = "Game Over!"
else -> "Invalid type!"
}
}
override fun render(delta: Float) {
clearScreen(red = 0.7f, green = 0.7f, blue = 0.7f)
batch.use {
font.draw(it, label, 10f, 10f)
}
}
}

View File

@@ -14,14 +14,15 @@ org.gradle.configureondemand=false
# Documented at: https://docs.gradle.org/current/userguide/command_line_interface.html#sec:command_line_logging
org.gradle.logging.level=quiet
ktxVersion=1.13.1-rc1
kotlinVersion=2.3.0
artemisOdbVersion=2.3.0
kotlinVersion=2.2.21
kotlinxCoroutinesVersion=1.8.1
aiVersion=1.8.2
artemisOdbVersion=2.3.0
ashleyVersion=1.7.4
kotlinxCoroutinesVersion=1.10.2
visUiVersion=1f8b37a24b
visUiVersion=1.5.7
android.useAndroidX=true
android.enableR8.fullMode=false
enableGraalNative=false
graalHelperVersion=2.0.1
gdxVersion=1.14.0
projectVersion=1.0.0

View File

@@ -1,12 +1,12 @@
#This file is generated by updateDaemonJvm
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/df211d3c3eefdc408b462041881bc575/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/93aeea858331bd6bb00ba94759830234/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/df211d3c3eefdc408b462041881bc575/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/b41931cf1e70bc8e08d7dd19c343ef00/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/3426ffcaa54c3f62406beb1f1ab8b179/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/d6690dfd71c4c91e08577437b5b2beb0/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/df211d3c3eefdc408b462041881bc575/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/b41931cf1e70bc8e08d7dd19c343ef00/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/1e91f45234d88a64dafb961c93ddc75a/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/552c7bffe0370c66410a51c55985b511/redirect
toolchainVersion=21
toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73c462e34475aeb6509ab7ba3eda218f/redirect
toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/54001d0b636ad500b432d20ef3d580d0/redirect
toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73c462e34475aeb6509ab7ba3eda218f/redirect
toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/54001d0b636ad500b432d20ef3d580d0/redirect
toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/29c55e6bad8a0049163f0184625cecd9/redirect
toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/3ac7a5361c25c0b23d933f44bdb0abd9/redirect
toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73c462e34475aeb6509ab7ba3eda218f/redirect
toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/54001d0b636ad500b432d20ef3d580d0/redirect
toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/28937bb8a7f83f57de92429a9a11c04e/redirect
toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/52fa104f4f641439587f75dd68b31bc2/redirect
toolchainVersion=17

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-milestone-1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -114,6 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -171,6 +172,7 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -210,7 +212,8 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.

View File

@@ -70,10 +70,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -0,0 +1,185 @@
buildscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath "io.github.fourlastor:construo:2.1.0"
if(enableGraalNative == 'true') {
classpath "org.graalvm.buildtools.native:org.graalvm.buildtools.native.gradle.plugin:0.9.28"
}
}
}
plugins {
id "application"
}
apply plugin: 'io.github.fourlastor.construo'
apply plugin: 'org.jetbrains.kotlin.jvm'
import io.github.fourlastor.construo.Target
sourceSets.main.resources.srcDirs += [ rootProject.file('assets').path ]
application.mainClass = 'u0012604.tictactoe.lwjgl3.Lwjgl3Launcher'
eclipse.project.name = appName + '-lwjgl3'
java.sourceCompatibility = 8
java.targetCompatibility = 8
if (JavaVersion.current().isJava9Compatible()) {
compileJava.options.release.set(8)
}
kotlin.compilerOptions.jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8)
dependencies {
implementation "com.badlogicgames.gdx:gdx-backend-lwjgl3:$gdxVersion"
implementation "com.badlogicgames.gdx:gdx-box2d-platform:$gdxVersion:natives-desktop"
implementation "com.badlogicgames.gdx:gdx-freetype-platform:$gdxVersion:natives-desktop"
implementation "com.badlogicgames.gdx:gdx-lwjgl3-angle:$gdxVersion"
implementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop"
implementation project(':core')
if(enableGraalNative == 'true') {
implementation "io.github.berstanio:gdx-svmhelper-backend-lwjgl3:$graalHelperVersion"
implementation "io.github.berstanio:gdx-svmhelper-extension-freetype:$graalHelperVersion"
}
}
def os = System.properties['os.name'].toLowerCase(Locale.ROOT)
run {
workingDir = rootProject.file('assets').path
// You can uncomment the next line if your IDE claims a build failure even when the app closed properly.
//setIgnoreExitValue(true)
if (os.contains('mac')) jvmArgs += "-XstartOnFirstThread"
}
jar {
// sets the name of the .jar file this produces to the name of the game or app, with the version after.
archiveFileName.set("${appName}-${projectVersion}.jar")
// the duplicatesStrategy matters starting in Gradle 7.0; this setting works.
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
dependsOn configurations.runtimeClasspath
from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
// these "exclude" lines remove some unnecessary duplicate files in the output JAR.
exclude('META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA')
dependencies {
exclude('META-INF/INDEX.LIST', 'META-INF/maven/**')
}
// setting the manifest makes the JAR runnable.
// enabling native access helps avoid a warning when Java 24 or later runs the JAR.
manifest {
attributes 'Main-Class': application.mainClass, 'Enable-Native-Access': 'ALL-UNNAMED'
}
// this last step may help on some OSes that need extra instruction to make runnable JARs.
doLast {
file(archiveFile).setExecutable(true, false)
}
}
// Builds a JAR that only includes the files needed to run on macOS, not Windows or Linux.
// The file size for a Mac-only JAR is about 7MB smaller than a cross-platform JAR.
tasks.register("jarMac") {
dependsOn("jar")
group("build")
jar.archiveFileName.set("${appName}-${projectVersion}-mac.jar")
jar.exclude("windows/x86/**", "windows/x64/**", "linux/arm32/**", "linux/arm64/**", "linux/x64/**", "**/*.dll", "**/*.so",
'META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA')
dependencies {
jar.exclude("windows/x86/**", "windows/x64/**", "linux/arm32/**", "linux/arm64/**", "linux/x64/**",
'META-INF/INDEX.LIST', 'META-INF/maven/**')
}
}
// Builds a JAR that only includes the files needed to run on Linux, not Windows or macOS.
// The file size for a Linux-only JAR is about 5MB smaller than a cross-platform JAR.
tasks.register("jarLinux") {
dependsOn("jar")
group("build")
jar.archiveFileName.set("${appName}-${projectVersion}-linux.jar")
jar.exclude("windows/x86/**", "windows/x64/**", "macos/arm64/**", "macos/x64/**", "**/*.dll", "**/*.dylib",
'META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA')
dependencies {
jar.exclude("windows/x86/**", "windows/x64/**", "macos/arm64/**", "macos/x64/**",
'META-INF/INDEX.LIST', 'META-INF/maven/**')
}
}
// Builds a JAR that only includes the files needed to run on Windows, not Linux or macOS.
// The file size for a Windows-only JAR is about 6MB smaller than a cross-platform JAR.
tasks.register("jarWin") {
dependsOn("jar")
group("build")
jar.archiveFileName.set("${appName}-${projectVersion}-win.jar")
jar.exclude("macos/arm64/**", "macos/x64/**", "linux/arm32/**", "linux/arm64/**", "linux/x64/**", "**/*.dylib", "**/*.so",
'META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA')
dependencies {
jar.exclude("macos/arm64/**", "macos/x64/**", "linux/arm32/**", "linux/arm64/**", "linux/x64/**",
'META-INF/INDEX.LIST', 'META-INF/maven/**')
}
}
construo {
// name of the executable
name.set(appName)
// human-readable name, used for example in the `.app` name for macOS
humanName.set(appName)
targets.configure {
register("linuxX64", Target.Linux) {
architecture.set(Target.Architecture.X86_64)
jdkUrl.set("https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_x64_linux_hotspot_17.0.15_6.tar.gz")
// Linux does not currently have a way to set the icon on the executable
}
register("macM1", Target.MacOs) {
architecture.set(Target.Architecture.AARCH64)
jdkUrl.set("https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_aarch64_mac_hotspot_17.0.15_6.tar.gz")
// macOS needs an identifier
identifier.set("u0012604.tictactoe." + appName)
// Optional: icon for macOS, as an ICNS file
macIcon.set(project.file("icons/logo.icns"))
}
register("macX64", Target.MacOs) {
architecture.set(Target.Architecture.X86_64)
jdkUrl.set("https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_x64_mac_hotspot_17.0.15_6.tar.gz")
// macOS needs an identifier
identifier.set("u0012604.tictactoe." + appName)
// Optional: icon for macOS, as an ICNS file
macIcon.set(project.file("icons/logo.icns"))
}
register("winX64", Target.Windows) {
architecture.set(Target.Architecture.X86_64)
// Optional: icon for Windows, as a PNG
icon.set(project.file("icons/logo.png"))
jdkUrl.set("https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.15%2B6/OpenJDK17U-jdk_x64_windows_hotspot_17.0.15_6.zip")
// Uncomment the next line to show a console when the game runs, to print messages.
//useConsole.set(true)
}
}
}
// Equivalent to the jar task; here for compatibility with gdx-setup.
tasks.register('dist') {
dependsOn 'jar'
}
distributions {
main {
contents {
into('libs') {
project.configurations.runtimeClasspath.files.findAll { file ->
file.getName() != project.tasks.jar.outputs.files.singleFile.name
}.each { file ->
exclude file.name
}
}
}
}
}
startScripts.dependsOn(':lwjgl3:jar')
startScripts.classpath = project.tasks.jar.outputs.files
if(enableGraalNative == 'true') {
apply from: file("nativeimage.gradle")
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,54 @@
project(":lwjgl3") {
apply plugin: "org.graalvm.buildtools.native"
graalvmNative {
binaries {
main {
imageName = appName
mainClass = application.mainClass
requiredVersion = '23.0'
buildArgs.add("-march=compatibility")
jvmArgs.addAll("-Dfile.encoding=UTF8")
sharedLibrary = false
resources.autodetect()
}
}
}
run {
doNotTrackState("Running the app should not be affected by Graal.")
}
// Modified from https://lyze.dev/2021/04/29/libGDX-Internal-Assets-List/ ; thanks again, Lyze!
// This creates a resource-config.json file based on the contents of the assets folder (and the libGDX icons).
// This file is used by Graal Native to embed those specific files.
// This has to run before nativeCompile, so it runs at the start of an unrelated resource-handling command.
generateResourcesConfigFile.doFirst {
def assetsFolder = new File("${project.rootDir}/assets/")
def lwjgl3 = project(':lwjgl3')
def resFolder = new File("${lwjgl3.projectDir}/src/main/resources/META-INF/native-image/${lwjgl3.ext.appName}")
resFolder.mkdirs()
def resFile = new File(resFolder, "resource-config.json")
resFile.delete()
resFile.append(
"""{
"resources":{
"includes":[
{
"pattern": ".*(""")
// This adds every filename in the assets/ folder to a pattern that adds those files as resources.
fileTree(assetsFolder).each {
// The backslash-Q and backslash-E escape the start and end of a literal string, respectively.
resFile.append("\\\\Q${it.name}\\\\E|")
}
// We also match all of the window icon images this way and the font files that are part of libGDX.
resFile.append(
"""libgdx.+\\\\.png|lsans.+)"
}
]},
"bundles":[]
}"""
)
}
}

View File

@@ -0,0 +1,40 @@
@file:JvmName("Lwjgl3Launcher")
package u0012604.tictactoe.lwjgl3
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration
import u0012604.tictactoe.Main
/** Launches the desktop (LWJGL3) application. */
fun main() {
// This handles macOS support and helps on Windows.
if (StartupHelper.startNewJvmIfRequired())
return
Lwjgl3Application(Main(), Lwjgl3ApplicationConfiguration().apply {
setTitle("NetworkedTicTacToe")
//// Vsync limits the frames per second to what your hardware can display, and helps eliminate
//// screen tearing. This setting doesn't always work on Linux, so the line after is a safeguard.
useVsync(true)
//// Limits FPS to the refresh rate of the currently active monitor, plus 1 to try to match fractional
//// refresh rates. The Vsync setting above should limit the actual FPS to match the monitor.
setForegroundFPS(Lwjgl3ApplicationConfiguration.getDisplayMode().refreshRate + 1)
//// If you remove the above line and set Vsync to false, you can get unlimited FPS, which can be
//// useful for testing performance, but can also be very stressful to some hardware.
//// You may also need to configure GPU drivers to fully disable Vsync; this can cause screen tearing.
setWindowedMode(640, 480)
//// You can change these files; they are in lwjgl3/src/main/resources/ .
//// They can also be loaded from the root of assets/ .
setWindowIcon(*(arrayOf(128, 64, 32, 16).map { "libgdx$it.png" }.toTypedArray()))
//// This should improve compatibility with Windows machines with buggy OpenGL drivers, Macs
//// with Apple Silicon that have to emulate compatibility with OpenGL anyway, and more.
//// This uses the dependency `com.badlogicgames.gdx:gdx-lwjgl3-angle` to function.
//// You can choose to remove the following line and the mentioned dependency if you want; they
//// are not intended for games that use GL30 (which is compatibility with OpenGL ES 3.0).
setOpenGLEmulation(Lwjgl3ApplicationConfiguration.GLEmulation.ANGLE_GLES20, 0, 0)
})
}

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2020 damios
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//Note, the above license and copyright applies to this file only.
package u0012604.tictactoe.lwjgl3
import com.badlogic.gdx.Version
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3NativesLoader
import org.lwjgl.system.JNI
import org.lwjgl.system.macosx.LibC
import org.lwjgl.system.macosx.ObjCRuntime
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.lang.management.ManagementFactory
import java.util.*
/**
* Adds some utilities to ensure that the JVM was started with the
* `-XstartOnFirstThread` argument, which is required on macOS for LWJGL 3
* to function. Also helps on Windows when users have names with characters from
* outside the Latin alphabet, a common cause of startup crashes.
*
* [Based on this java-gaming.org post by kappa](https://jvm-gaming.org/t/starting-jvm-on-mac-with-xstartonfirstthread-programmatically/57547)
* @author damios
*/
class StartupHelper private constructor() {
init {
throw UnsupportedOperationException()
}
companion object {
private const val JVM_RESTARTED_ARG = "jvmIsRestarted"
/**
* Starts a new JVM if the application was started on macOS without the
* `-XstartOnFirstThread` argument. This also includes some code for
* Windows, for the case where the user's home directory includes certain
* non-Latin-alphabet characters (without this code, most LWJGL3 apps fail
* immediately for those users). Returns whether a new JVM was started and
* thus no code should be executed.
*
* **Usage:**
*
* ```
* fun main() {
* if (StartupHelper.startNewJvmIfRequired(true)) return // This handles macOS support and helps on Windows.
* // after this is the actual main method code
* }
* ```
*
* @param redirectOutput
* whether the output of the new JVM should be rerouted to the
* old JVM, so it can be accessed in the same place; keeps the
* old JVM running if enabled
* @return whether a new JVM was started and thus no code should be executed
* in this one
*/
@JvmOverloads
fun startNewJvmIfRequired(redirectOutput: Boolean = true): Boolean {
val osName = System.getProperty("os.name").lowercase(Locale.ROOT)
if (!osName.contains("mac")) {
if (osName.contains("windows")) {
// Here, we are trying to work around an issue with how LWJGL3 loads its extracted .dll files.
// By default, LWJGL3 extracts to the directory specified by "java.io.tmpdir", which is usually the user's home.
// If the user's name has non-ASCII (or some non-alphanumeric) characters in it, that would fail.
// By extracting to the relevant "ProgramData" folder, which is usually "C:\ProgramData", we avoid this.
// We also temporarily change the "user.name" property to one without any chars that would be invalid.
// We revert our changes immediately after loading LWJGL3 natives.
val programData = System.getenv("ProgramData") ?: "C:\\Temp\\"
val prevTmpDir = System.getProperty("java.io.tmpdir", programData)
val prevUser = System.getProperty("user.name", "libGDX_User")
System.setProperty("java.io.tmpdir", "$programData/libGDX-temp")
System.setProperty("user.name", "User_${prevUser.hashCode()}_GDX${Version.VERSION}".replace('.', '_'))
Lwjgl3NativesLoader.load()
System.setProperty("java.io.tmpdir", prevTmpDir)
System.setProperty("user.name", prevUser)
}
return false
}
// There is no need for -XstartOnFirstThread on Graal native image
if (System.getProperty("org.graalvm.nativeimage.imagecode", "").isNotEmpty()) {
return false
}
// Checks if we are already on the main thread, such as from running via Construo.
val objcMsgSend = ObjCRuntime.getLibrary().getFunctionAddress("objc_msgSend")
val nsThread = ObjCRuntime.objc_getClass("NSThread")
val currentThread = JNI.invokePPP(nsThread, ObjCRuntime.sel_getUid("currentThread"), objcMsgSend)
val isMainThread = JNI.invokePPZ(currentThread, ObjCRuntime.sel_getUid("isMainThread"), objcMsgSend)
if (isMainThread) return false
val pid = LibC.getpid()
// check whether -XstartOnFirstThread is enabled
if ("1" == System.getenv("JAVA_STARTED_ON_FIRST_THREAD_$pid")) {
return false
}
// check whether the JVM was previously restarted
// avoids looping, but most certainly leads to a crash
if ("true" == System.getProperty(JVM_RESTARTED_ARG)) {
System.err.println(
"There was a problem evaluating whether the JVM was started with the -XstartOnFirstThread argument."
)
return false
}
// Restart the JVM with -XstartOnFirstThread
val jvmArgs = ArrayList<String?>()
val separator = System.getProperty("file.separator", "/")
// The following line is used assuming you target Java 8, the minimum for LWJGL3.
val javaExecPath = System.getProperty("java.home") + separator + "bin" + separator + "java"
// If targeting Java 9 or higher, you could use the following instead of the above line:
//String javaExecPath = ProcessHandle.current().info().command().orElseThrow();
if (!File(javaExecPath).exists()) {
System.err.println(
"A Java installation could not be found. If you are distributing this app with a bundled JRE, be sure to set the -XstartOnFirstThread argument manually!"
)
return false
}
jvmArgs.add(javaExecPath)
jvmArgs.add("-XstartOnFirstThread")
jvmArgs.add("-D$JVM_RESTARTED_ARG=true")
jvmArgs.addAll(ManagementFactory.getRuntimeMXBean().inputArguments)
jvmArgs.add("-cp")
jvmArgs.add(System.getProperty("java.class.path"))
var mainClass = System.getenv("JAVA_MAIN_CLASS_$pid")
if (mainClass == null) {
val trace = Thread.currentThread().stackTrace
mainClass = if (trace.isNotEmpty()) {
trace[trace.size - 1].className
} else {
System.err.println("The main class could not be determined.")
return false
}
}
jvmArgs.add(mainClass)
try {
if (!redirectOutput) {
val processBuilder = ProcessBuilder(jvmArgs)
processBuilder.start()
} else {
val process = ProcessBuilder(jvmArgs)
.redirectErrorStream(true).start()
val processOutput = BufferedReader(
InputStreamReader(process.inputStream)
)
var line: String?
while (processOutput.readLine().also { line = it } != null) {
println(line)
}
process.waitFor()
}
} catch (e: Exception) {
System.err.println("There was a problem restarting the JVM")
e.printStackTrace()
}
return true
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -5,4 +5,4 @@ plugins {
// A list of which subprojects to load as part of the same larger project.
// You can remove Strings from the list and reload the Gradle project
// if you want to temporarily disable a subproject.
include 'android', 'core'
include 'android', 'core', 'lwjgl3'

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -2,7 +2,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:glEsVersion="0x00020000" android:required="true"/>
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:fullBackupContent="true"
@@ -15,7 +14,7 @@
<activity
android:name="com.iofferyoutea.WitchQueen.android.AndroidLauncher"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:screenOrientation="landscape"
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|screenLayout"
android:exported="true">
<intent-filter>

View File

@@ -12,7 +12,7 @@ class AndroidLauncher : AndroidApplication() {
super.onCreate(savedInstanceState)
initialize(Main(), AndroidApplicationConfiguration().apply {
// Configure your application here.
//useImmersiveMode = true // Recommended, but not required.
useImmersiveMode = true // Recommended, but not required.
})
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 B

View File

@@ -7,6 +7,7 @@ dependencies {
api "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
api "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
api "com.badlogicgames.gdx:gdx:$gdxVersion"
api "com.github.kotcrab.vis-ui:vis-ui:$visUiVersion"
api "io.github.libktx:ktx-actors:$ktxVersion"
api "io.github.libktx:ktx-ai:$ktxVersion"
api "io.github.libktx:ktx-app:$ktxVersion"

View File

@@ -1,6 +0,0 @@
package com.iofferyoutea.WitchQueen
class Client(val hostAddress: String, val hostPort: Int, val playerProfile: PlayerProfile) {
val preparedItems: MutableList<Int> = mutableListOf(0, 0, 0, 0) // Item ID. 0 for no item
lateinit var map: Map
}

View File

@@ -1,4 +0,0 @@
package com.iofferyoutea.WitchQueen
class Game(val clients: Array<Client>) {
}

View File

@@ -1,6 +1,5 @@
package com.iofferyoutea.WitchQueen
import com.badlogic.gdx.Game
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.Texture.TextureFilter.Linear
import com.badlogic.gdx.graphics.g2d.SpriteBatch
@@ -11,14 +10,13 @@ import ktx.assets.disposeSafely
import ktx.assets.toInternalFile
import ktx.async.KtxAsync
import ktx.graphics.use
import java.lang.ref.WeakReference
class Main : KtxGame<KtxScreen>() {
override fun create() {
KtxAsync.initiate()
addScreen(MainMenu())
setScreen<MainMenu>()
addScreen(FirstScreen())
setScreen<FirstScreen>()
}
}

View File

@@ -1,92 +0,0 @@
package com.iofferyoutea.WitchQueen
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.BitmapFont
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.ui.Container
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.scenes.scene2d.ui.VerticalGroup
import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable
import ktx.app.KtxScreen
import ktx.app.clearScreen
import org.w3c.dom.Text
class MainMenu : KtxScreen {
val table = Table().apply {
debug = true
setFillParent(true)
}
val stage = Stage().apply {
Gdx.input.inputProcessor = this
addActor(table)
}
// Default
val defaultButtonTrd = TextureRegionDrawable(Texture("default.png"))
val flippedDefaultButtonTrd = TextureRegionDrawable(Texture("default_flipped.png"))
val defaultTextButtonStyle = TextButton.TextButtonStyle (
defaultButtonTrd,
flippedDefaultButtonTrd,
defaultButtonTrd,
BitmapFont()
)
// Map
// Lobby
// Start off only showing Host and Join button. When selected show menu for that option.
// Host is normal lobby
// Join will bring up menu with games on local network
val hostButton = TextButton("Host", defaultTextButtonStyle)
val joinButton = TextButton("Join", defaultTextButtonStyle)
val hostOrJoinVerticalGroup = VerticalGroup().apply {
addActor(hostButton)
addActor(joinButton)
}
val lobbyContainer = Container<Actor>(hostOrJoinVerticalGroup)
//region Play
val casualButton = TextButton("Casual", defaultTextButtonStyle)
//endregion
val everythingVerticalGroup = VerticalGroup().apply {
addActor(lobbyContainer)
addActor(casualButton)
table.add(this)
Gdx.app.log("MainMenu", stage.width.toString())
//Gdx.app.log("MainMenu", table.width.toString())
}
private fun update(delta: Float) {
if (hostButton.isPressed) {
Gdx.app.log("MainMenu", "Host button pressed")
}
if (joinButton.isPressed) {
Gdx.app.log("MainMenu", "Join button pressed")
}
if (casualButton.isPressed) {
Gdx.app.log("MainMenu", "Casual button pressed")
}
}
override fun render(delta: Float) {
clearScreen(0f,0f, 0f)
update(delta)
stage.act(delta)
stage.draw()
}
override fun resize(width: Int, height: Int) {
Gdx.app.log("MainMenu", "resize called! New width: $width, new height $height")
everythingVerticalGroup.setSize(width.toFloat(), height.toFloat())
}
override fun dispose() {
stage.dispose()
}
}

View File

@@ -1,6 +0,0 @@
package com.iofferyoutea.WitchQueen
class Map(val mapType: MapType) {
val rooms = Array(2) { Array<Room>(2) { Room(mapType) } } // We use an array instead of list i think
}

View File

@@ -1,5 +0,0 @@
package com.iofferyoutea.WitchQueen
enum class MapType {
DUNGEON, STREETS
}

View File

@@ -1,10 +0,0 @@
package com.iofferyoutea.WitchQueen
enum class PlayerState {
IDLE, LOOTING, FIGHTING
}
class Player(val map: Map) {
var currentState = PlayerState.IDLE
val currentRoom = arrayOf(0, 0) // Make this a spawn room or something
}

View File

@@ -1,7 +0,0 @@
package com.iofferyoutea.WitchQueen
class PlayerProfile {
var iconPath = "default.png"
var username = "Default"
val availableItems: MutableList<Int> = mutableListOf() // Use Item IDs?
}

View File

@@ -1,12 +0,0 @@
package com.iofferyoutea.WitchQueen
enum class RoomActivity {
EMPTY, LOOT, FIGHT
}
class Room(val mapType: MapType) {
var availableActivities: MutableList<RoomActivity> = mutableListOf()
init {
// Generate Room in this init block!
}
}

View File

@@ -1,75 +0,0 @@
package com.iofferyoutea.uitest
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.graphics.Texture
import com.badlogic.gdx.graphics.g2d.BitmapFont
import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.ui.Container
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable
import ktx.app.KtxGame
import ktx.app.KtxScreen
import ktx.app.clearScreen
import ktx.async.KtxAsync
import org.w3c.dom.Text
class Main : KtxGame<KtxScreen>() {
override fun create() {
KtxAsync.initiate()
addScreen(FirstScreen())
setScreen<FirstScreen>()
}
}
class FirstScreen : KtxScreen {
val stage = Stage()
val table = Table()
//region Label
val labelStyle = Label.LabelStyle(
BitmapFont(),
Color.BLACK
)
val label = Label("My Label", labelStyle)
//endregion
//region TextButton
val textButtonStyle = TextButton.TextButtonStyle(
TextureRegionDrawable(Texture("logo.png")),
TextureRegionDrawable(Texture("logo.png")),
TextureRegionDrawable(Texture("logo.png")),
BitmapFont()
)
val textButton = TextButton("My Text Button", textButtonStyle)
//endregion
override fun show() {
stage.addActor(table)
table.setFillParent(true)
table.debug = true
label.wrap = false
label.setFontScale(5f) // Label text size must be done through setFontScale(f). Maybe we can do like this cell's width / 1080->? Or whatever would equal 1 on the dev machine
table.add(label)
table.getCell<Label>(label).width(1200f).height(400f) // Widget sizes are meant to be controlled through their parent like we do here
table.row()
table.add(textButton)
table.getCell<TextButton>(textButton).width(100f).height(600f)
}
override fun render(delta: Float) {
clearScreen(red = 0.7f, green = 0.7f, blue = 0.7f)
stage.act()
stage.draw()
}
override fun dispose() {
stage.dispose()
}
}

Binary file not shown.