diff --git a/README.md b/README.md
index 04f5af9..0ab5659 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,33 @@
-# OBSOLETE: Android Konfigurator
+# Android Konfigurator
-
+
-This project was archived on March 12, 2026. The functionality formerly provided by this app has been moved to the [Konfigurator Subapp](https://git.ktag.clubk.club/Software/Android-KTag-Apps/src/branch/main/subapp-konfigurator) of the new [KTag Apps](https://git.ktag.clubk.club/Software/Android-KTag-Apps). All of the existing Android apps have been consolidated into the main KTag Apps repository to simplify maintenance and provide a single distribution for related functionality.
+## Overview
-What this means
----------------
+This software is used for configuring and initiating KTag games.
-- No further development, feature work, or bug fixes will be applied to this repository.
+The primary documentation for KTag is on the KTag website at https://ktag.clubk.club/.
+You can ask questions (and get answers!) about this software on the KTag forum at https://forum.ktag.clubk.club/c/software/.
-License
--------
+## License: [AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later.html)
-This project is covered by the repository LICENSE. See the [`LICENSE`](LICENSE) file for full terms.
+This software is part of the KTag project, a DIY laser tag game with customizable features and wide interoperability.
-Contact
--------
+🛡️ 🃞
-For questions about migration, post in the KTag forum (in the `Software` category): https://forum.ktag.clubk.club/c/software/6.
+Copyright © 2025 Joseph P. Kearney and the KTag developers.
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Affero General Public License as published by the Free
+Software Foundation, either version 3 of the License, or (at your option) any
+later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT
+ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+details.
+
+There should be a copy of the GNU Affero General Public License in the [LICENSE](LICENSE)
+file in the root of this repository. If not, see .
diff --git a/app/src/main/java/club/clubk/ktag/konfigurator/Device.kt b/app/src/main/java/club/clubk/ktag/konfigurator/Device.kt
index 8b607aa..2b953fe 100644
--- a/app/src/main/java/club/clubk/ktag/konfigurator/Device.kt
+++ b/app/src/main/java/club/clubk/ktag/konfigurator/Device.kt
@@ -2,14 +2,60 @@ package club.clubk.ktag.konfigurator
import java.util.UUID
+const val EDIT_DELAY_TIME: Long = 3L
+
+sealed class DeviceState {
+ data object Configurable : DeviceState()
+ data object Ready : DeviceState()
+ data object Playing : DeviceState()
+ data object WrapUp : DeviceState()
+}
+
+sealed class DeviceConfigureState {
+ data object Discovered: DeviceConfigureState()
+ data object Configuring: DeviceConfigureState()
+ data object Success: DeviceConfigureState()
+ data object Failure: DeviceConfigureState()
+}
+
+enum class DeviceParameter {
+ PLAYER_ID, TEAM, SECONDARY_COLOR, MAX_HEALTH, SPECIAL_WEAPONS
+}
+
data class Device(val uuid: UUID = UUID.randomUUID(),
var name: String = "Unknown Device",
- var address: String = "FF:FF:FF:FF:FF:FF",
- var deviceType : Int? = null,
- var team : Int? = null,
- var playerID : Int? = null,
- var deviceState: DeviceState? = null
+ var address: String = "00:00:00:00:00:00",
+ var deviceType: Int? = null,
+ var deviceState: DeviceState? = null,
+ // All configurable variables
+ private var _playerID: Int? = null,
+ private var _team: Int? = null,
+ private var _secondaryColor: Int? = null,
+ private var _maxHealth: Int? = null,
+ private var _specialWeapons: Int? = null
) {
+ var deviceConfigureState: DeviceConfigureState = DeviceConfigureState.Discovered
+ private set
+
+ var dirtyPlayerID: Boolean = false
+ private set
+ var dirtyTeam: Boolean = false
+ private set
+ var dirtySecondaryColor: Boolean = false
+ private set
+ var dirtyMaxHealth: Boolean = false
+ private set
+ var dirtySpecialWeapons: Boolean = false
+ private set
+
+ var lastEditTime: Long = 0L
+ private set
+
+ val playerID: Int? get() = _playerID
+ val team: Int? get() = _team
+ val secondaryColor: Int? get() = _secondaryColor
+ val maxHealth: Int? get() = _maxHealth
+ val specialWeapons: Int? get() = _specialWeapons
fun deviceTypeName(): String {
return when(deviceType) {
@@ -20,5 +66,67 @@ data class Device(val uuid: UUID = UUID.randomUUID(),
else -> "Unknown Device Type"
}
}
-}
+ fun deviceTypeDrawable() {
+ return
+ }
+
+ fun isDirty(): Boolean {
+ return dirtyPlayerID || dirtyTeam || dirtySecondaryColor
+ || dirtyMaxHealth || dirtySpecialWeapons
+ }
+
+ fun isPastEditTime(): Boolean {
+ return System.nanoTime() - lastEditTime!! >= EDIT_DELAY_TIME
+ }
+
+ fun setParameter(parameter: DeviceParameter, value: Any) {
+ lastEditTime = System.nanoTime()
+ when (parameter) {
+ DeviceParameter.PLAYER_ID -> {
+ _playerID = value as Int
+ dirtyPlayerID = true
+ }
+ DeviceParameter.TEAM -> {
+ _team = value as Int
+ dirtyTeam = true
+ }
+ DeviceParameter.SECONDARY_COLOR -> {
+ _secondaryColor = value as Int
+ dirtySecondaryColor = true
+ }
+ DeviceParameter.MAX_HEALTH -> {
+ _maxHealth = value as Int
+ dirtyMaxHealth = true
+ }
+ DeviceParameter.SPECIAL_WEAPONS -> {
+ _specialWeapons = value as Int
+ dirtySpecialWeapons = true
+ }
+ }
+ }
+
+ fun checkForDirt() {
+ if (isDirty() && isPastEditTime()) {
+ return
+ }
+ }
+
+ fun getDirtyParameters(): List {
+ val changed = mutableListOf()
+ if (dirtyPlayerID) changed.add(DeviceParameter.PLAYER_ID)
+ if (dirtyTeam) changed.add(DeviceParameter.TEAM)
+ if (dirtySecondaryColor) changed.add(DeviceParameter.SECONDARY_COLOR)
+ if (dirtyMaxHealth) changed.add(DeviceParameter.MAX_HEALTH)
+ if (dirtySpecialWeapons) changed.add(DeviceParameter.SPECIAL_WEAPONS)
+ return changed
+ }
+
+ fun clearDirt() {
+ dirtyPlayerID = false
+ dirtyTeam = false
+ dirtySecondaryColor = false
+ dirtyMaxHealth = false
+ dirtySpecialWeapons = false
+ }
+}
diff --git a/app/src/main/java/club/clubk/ktag/konfigurator/DeviceState.kt b/app/src/main/java/club/clubk/ktag/konfigurator/DeviceState.kt
deleted file mode 100644
index 22ea035..0000000
--- a/app/src/main/java/club/clubk/ktag/konfigurator/DeviceState.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package club.clubk.ktag.konfigurator
-
-sealed class DeviceState {
- object Configurable : DeviceState()
- object Ready : DeviceState()
- object Playing : DeviceState()
- object WrapUp : DeviceState()
-}
\ No newline at end of file
diff --git a/app/src/main/java/club/clubk/ktag/konfigurator/GameConfig.kt b/app/src/main/java/club/clubk/ktag/konfigurator/GameConfig.kt
index ccf6988..47e96a0 100644
--- a/app/src/main/java/club/clubk/ktag/konfigurator/GameConfig.kt
+++ b/app/src/main/java/club/clubk/ktag/konfigurator/GameConfig.kt
@@ -5,5 +5,5 @@ data class GameConfig(var name: String = "Default",
var pregameLength: Int = 60000,
var numRounds: Int = 2,
var maxHealth: Int = 10,
- var numBombs: Int = 1 // Special Weapons Received on Game Reentry
+ var specialWeapons: Int = 1 // Special Weapons Received on Game Reentry
)
diff --git a/app/src/main/java/club/clubk/ktag/konfigurator/MainActivity.kt b/app/src/main/java/club/clubk/ktag/konfigurator/MainActivity.kt
index d67932b..e714f6a 100644
--- a/app/src/main/java/club/clubk/ktag/konfigurator/MainActivity.kt
+++ b/app/src/main/java/club/clubk/ktag/konfigurator/MainActivity.kt
@@ -1,8 +1,6 @@
package club.clubk.ktag.konfigurator
import android.Manifest
-import android.bluetooth.BluetoothManager
-import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
@@ -23,7 +21,6 @@ import androidx.core.content.ContextCompat
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
@@ -253,7 +250,7 @@ fun GameConfigEditor(oldGameConfig: GameConfig,
var pregameLength by rememberSaveable(oldGameConfig.pregameLength) { mutableStateOf(oldGameConfig.pregameLength.toString()) }
var numRounds by rememberSaveable(oldGameConfig.numRounds) { mutableStateOf(oldGameConfig.numRounds.toString()) }
var maxHealth by rememberSaveable(oldGameConfig.maxHealth) { mutableStateOf(oldGameConfig.maxHealth.toString()) }
- var numBombs by rememberSaveable(oldGameConfig.numBombs) { mutableStateOf(oldGameConfig.numBombs.toString()) }
+ var numBombs by rememberSaveable(oldGameConfig.specialWeapons) { mutableStateOf(oldGameConfig.specialWeapons.toString()) }
// For tracking validation errors
var gameLengthError by rememberSaveable { mutableStateOf(false) }
@@ -283,7 +280,7 @@ fun GameConfigEditor(oldGameConfig: GameConfig,
pregameLength = pregameLength.toIntOrNull() ?: oldGameConfig.pregameLength,
numRounds = numRounds.toIntOrNull() ?: oldGameConfig.numRounds,
maxHealth = maxHealth.toIntOrNull() ?: oldGameConfig.maxHealth,
- numBombs = numBombs.toIntOrNull() ?: oldGameConfig.numBombs
+ specialWeapons = numBombs.toIntOrNull() ?: oldGameConfig.specialWeapons
)
onSave(newGameConfig)
}
diff --git a/app/src/main/java/club/clubk/ktag/konfigurator/StateMachineViewModel.kt b/app/src/main/java/club/clubk/ktag/konfigurator/StateMachineViewModel.kt
index aeedd61..267dbf5 100644
--- a/app/src/main/java/club/clubk/ktag/konfigurator/StateMachineViewModel.kt
+++ b/app/src/main/java/club/clubk/ktag/konfigurator/StateMachineViewModel.kt
@@ -69,10 +69,12 @@ class StateMachineViewModel(context: Context) : ViewModel() {
when (packet) {
is HelloPacket -> {
// Log.d(TAG_BLE_SCAN, "HelloPacket scanned")
- scannedDevice.name = packet.deviceName
- scannedDevice.deviceType = packet.deviceType
- scannedDevice.team = packet.teamId
- scannedDevice.deviceState = DeviceState.Configurable
+ val scannedDevice = Device(
+ name = packet.deviceName,
+ address = result.device.address,
+ deviceType = packet.deviceType,
+ deviceState = DeviceState.Configurable)
+ scannedDevice.setParameter(DeviceParameter.TEAM, packet.teamId)
addOrRefreshDevice(scannedDevice)
}
is ConsolePacket -> {
@@ -148,9 +150,13 @@ class StateMachineViewModel(context: Context) : ViewModel() {
var oldDevice = currentDevices[index]
newDevice.name = oldDevice.name
newDevice.deviceType = oldDevice.deviceType ?: newDevice.deviceType
- newDevice.team = oldDevice.team ?: newDevice.team
- newDevice.playerID = oldDevice.playerID ?: newDevice.playerID
newDevice.deviceState = newDevice.deviceState ?: oldDevice.deviceState
+ oldDevice.team?.let { teamValue ->
+ newDevice.setParameter(DeviceParameter.TEAM, teamValue)
+ }
+ oldDevice.playerID?.let { playerIDValue ->
+ newDevice.setParameter(DeviceParameter.PLAYER_ID, playerIDValue)
+ }
currentDevices[index] = newDevice
}
_devices.value = currentDevices
@@ -165,18 +171,26 @@ class StateMachineViewModel(context: Context) : ViewModel() {
if (index == -1) { return }
var oldDevice = currentDevices[index]
newDevice.deviceType = newDevice.deviceType ?: oldDevice.deviceType
- newDevice.team = newDevice.team ?: oldDevice.team
- newDevice.playerID = newDevice.playerID ?: oldDevice.playerID
+ oldDevice.team?.let { teamValue ->
+ newDevice.setParameter(DeviceParameter.TEAM, teamValue)
+ }
+ oldDevice.playerID?.let { playerIDValue ->
+ newDevice.setParameter(DeviceParameter.PLAYER_ID, playerIDValue)
+ }
currentDevices[index] = newDevice
_devices.value = currentDevices
_allDevicesReady.value = allDevicesReady()
}
- fun updateDeviceTeam(deviceAddress: String, newTeam: Int) {
+ fun updateDeviceTeam(deviceAddress: String, newTeam: Int?) {
_devices.update { currentList ->
currentList.map { device ->
if (device.address == deviceAddress) {
- device.copy(team = newTeam) // Creates a new Device instance
+ val updatedDevice = device.copy()
+ if (newTeam != null) {
+ updatedDevice.setParameter(DeviceParameter.TEAM, newTeam)
+ }
+ updatedDevice // Return the modified device
} else {
device
}
@@ -187,7 +201,7 @@ class StateMachineViewModel(context: Context) : ViewModel() {
fun cycleDeviceTeam(device: Device) {
Log.d("STATEMACHINE", "cycling device team")
- var newTeam = device.team ?: -1
+ var newTeam: Int = (device.team?.toInt() ?: -1)
newTeam++
if (newTeam > 2) {
newTeam = 0
@@ -244,7 +258,7 @@ class StateMachineViewModel(context: Context) : ViewModel() {
parameterPacketGenerator.generatePacket(
targetAddress = device.address,
subtype = 2, // Request Parameter Change
- key1 = 1, value1 = teamId, // Key 1 is Team ID
+ key1 = 1, value1 = teamId.toInt(), // Key 1 is Team ID
key2 = 4, value2 = gameCfg.maxHealth // Key 2 is Max Health
)
// If a device for some reason can't be configured (e.g. missing address),
diff --git a/status_OBSOLETE.svg b/status_OBSOLETE.svg
deleted file mode 100644
index b037840..0000000
--- a/status_OBSOLETE.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file