Compare commits
4 commits
main
...
config_han
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0827bdc7b4 | ||
|
|
527ad08c28 | ||
| 74fc19c64d | |||
| 67426165b7 |
7 changed files with 166 additions and 45 deletions
35
README.md
35
README.md
|
|
@ -1,22 +1,33 @@
|
||||||
# OBSOLETE: Android Konfigurator
|
# Android Konfigurator
|
||||||
|
|
||||||

|
<img src="konfigurator.png" width=300px/>
|
||||||
|
|
||||||
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
|
🛡️ <https://ktag.clubk.club> 🃞
|
||||||
-------
|
|
||||||
|
|
||||||
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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,60 @@ package club.clubk.ktag.konfigurator
|
||||||
|
|
||||||
import java.util.UUID
|
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(),
|
data class Device(val uuid: UUID = UUID.randomUUID(),
|
||||||
var name: String = "Unknown Device",
|
var name: String = "Unknown Device",
|
||||||
var address: String = "FF:FF:FF:FF:FF:FF",
|
var address: String = "00:00:00:00:00:00",
|
||||||
var deviceType : Int? = null,
|
var deviceType: Int? = null,
|
||||||
var team : Int? = null,
|
var deviceState: DeviceState? = null,
|
||||||
var playerID : Int? = null,
|
// All configurable variables
|
||||||
var deviceState: DeviceState? = null
|
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 {
|
fun deviceTypeName(): String {
|
||||||
return when(deviceType) {
|
return when(deviceType) {
|
||||||
|
|
@ -20,5 +66,67 @@ data class Device(val uuid: UUID = UUID.randomUUID(),
|
||||||
else -> "Unknown Device Type"
|
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<DeviceParameter> {
|
||||||
|
val changed = mutableListOf<DeviceParameter>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package club.clubk.ktag.konfigurator
|
|
||||||
|
|
||||||
sealed class DeviceState {
|
|
||||||
object Configurable : DeviceState()
|
|
||||||
object Ready : DeviceState()
|
|
||||||
object Playing : DeviceState()
|
|
||||||
object WrapUp : DeviceState()
|
|
||||||
}
|
|
||||||
|
|
@ -5,5 +5,5 @@ data class GameConfig(var name: String = "Default",
|
||||||
var pregameLength: Int = 60000,
|
var pregameLength: Int = 60000,
|
||||||
var numRounds: Int = 2,
|
var numRounds: Int = 2,
|
||||||
var maxHealth: Int = 10,
|
var maxHealth: Int = 10,
|
||||||
var numBombs: Int = 1 // Special Weapons Received on Game Reentry
|
var specialWeapons: Int = 1 // Special Weapons Received on Game Reentry
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
package club.clubk.ktag.konfigurator
|
package club.clubk.ktag.konfigurator
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.bluetooth.BluetoothManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|
@ -23,7 +21,6 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
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 pregameLength by rememberSaveable(oldGameConfig.pregameLength) { mutableStateOf(oldGameConfig.pregameLength.toString()) }
|
||||||
var numRounds by rememberSaveable(oldGameConfig.numRounds) { mutableStateOf(oldGameConfig.numRounds.toString()) }
|
var numRounds by rememberSaveable(oldGameConfig.numRounds) { mutableStateOf(oldGameConfig.numRounds.toString()) }
|
||||||
var maxHealth by rememberSaveable(oldGameConfig.maxHealth) { mutableStateOf(oldGameConfig.maxHealth.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
|
// For tracking validation errors
|
||||||
var gameLengthError by rememberSaveable { mutableStateOf(false) }
|
var gameLengthError by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
@ -283,7 +280,7 @@ fun GameConfigEditor(oldGameConfig: GameConfig,
|
||||||
pregameLength = pregameLength.toIntOrNull() ?: oldGameConfig.pregameLength,
|
pregameLength = pregameLength.toIntOrNull() ?: oldGameConfig.pregameLength,
|
||||||
numRounds = numRounds.toIntOrNull() ?: oldGameConfig.numRounds,
|
numRounds = numRounds.toIntOrNull() ?: oldGameConfig.numRounds,
|
||||||
maxHealth = maxHealth.toIntOrNull() ?: oldGameConfig.maxHealth,
|
maxHealth = maxHealth.toIntOrNull() ?: oldGameConfig.maxHealth,
|
||||||
numBombs = numBombs.toIntOrNull() ?: oldGameConfig.numBombs
|
specialWeapons = numBombs.toIntOrNull() ?: oldGameConfig.specialWeapons
|
||||||
)
|
)
|
||||||
onSave(newGameConfig)
|
onSave(newGameConfig)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,10 +69,12 @@ class StateMachineViewModel(context: Context) : ViewModel() {
|
||||||
when (packet) {
|
when (packet) {
|
||||||
is HelloPacket -> {
|
is HelloPacket -> {
|
||||||
// Log.d(TAG_BLE_SCAN, "HelloPacket scanned")
|
// Log.d(TAG_BLE_SCAN, "HelloPacket scanned")
|
||||||
scannedDevice.name = packet.deviceName
|
val scannedDevice = Device(
|
||||||
scannedDevice.deviceType = packet.deviceType
|
name = packet.deviceName,
|
||||||
scannedDevice.team = packet.teamId
|
address = result.device.address,
|
||||||
scannedDevice.deviceState = DeviceState.Configurable
|
deviceType = packet.deviceType,
|
||||||
|
deviceState = DeviceState.Configurable)
|
||||||
|
scannedDevice.setParameter(DeviceParameter.TEAM, packet.teamId)
|
||||||
addOrRefreshDevice(scannedDevice)
|
addOrRefreshDevice(scannedDevice)
|
||||||
}
|
}
|
||||||
is ConsolePacket -> {
|
is ConsolePacket -> {
|
||||||
|
|
@ -148,9 +150,13 @@ class StateMachineViewModel(context: Context) : ViewModel() {
|
||||||
var oldDevice = currentDevices[index]
|
var oldDevice = currentDevices[index]
|
||||||
newDevice.name = oldDevice.name
|
newDevice.name = oldDevice.name
|
||||||
newDevice.deviceType = oldDevice.deviceType ?: newDevice.deviceType
|
newDevice.deviceType = oldDevice.deviceType ?: newDevice.deviceType
|
||||||
newDevice.team = oldDevice.team ?: newDevice.team
|
|
||||||
newDevice.playerID = oldDevice.playerID ?: newDevice.playerID
|
|
||||||
newDevice.deviceState = newDevice.deviceState ?: oldDevice.deviceState
|
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
|
currentDevices[index] = newDevice
|
||||||
}
|
}
|
||||||
_devices.value = currentDevices
|
_devices.value = currentDevices
|
||||||
|
|
@ -165,18 +171,26 @@ class StateMachineViewModel(context: Context) : ViewModel() {
|
||||||
if (index == -1) { return }
|
if (index == -1) { return }
|
||||||
var oldDevice = currentDevices[index]
|
var oldDevice = currentDevices[index]
|
||||||
newDevice.deviceType = newDevice.deviceType ?: oldDevice.deviceType
|
newDevice.deviceType = newDevice.deviceType ?: oldDevice.deviceType
|
||||||
newDevice.team = newDevice.team ?: oldDevice.team
|
oldDevice.team?.let { teamValue ->
|
||||||
newDevice.playerID = newDevice.playerID ?: oldDevice.playerID
|
newDevice.setParameter(DeviceParameter.TEAM, teamValue)
|
||||||
|
}
|
||||||
|
oldDevice.playerID?.let { playerIDValue ->
|
||||||
|
newDevice.setParameter(DeviceParameter.PLAYER_ID, playerIDValue)
|
||||||
|
}
|
||||||
currentDevices[index] = newDevice
|
currentDevices[index] = newDevice
|
||||||
_devices.value = currentDevices
|
_devices.value = currentDevices
|
||||||
_allDevicesReady.value = allDevicesReady()
|
_allDevicesReady.value = allDevicesReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateDeviceTeam(deviceAddress: String, newTeam: Int) {
|
fun updateDeviceTeam(deviceAddress: String, newTeam: Int?) {
|
||||||
_devices.update { currentList ->
|
_devices.update { currentList ->
|
||||||
currentList.map { device ->
|
currentList.map { device ->
|
||||||
if (device.address == deviceAddress) {
|
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 {
|
} else {
|
||||||
device
|
device
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +201,7 @@ class StateMachineViewModel(context: Context) : ViewModel() {
|
||||||
|
|
||||||
fun cycleDeviceTeam(device: Device) {
|
fun cycleDeviceTeam(device: Device) {
|
||||||
Log.d("STATEMACHINE", "cycling device team")
|
Log.d("STATEMACHINE", "cycling device team")
|
||||||
var newTeam = device.team ?: -1
|
var newTeam: Int = (device.team?.toInt() ?: -1)
|
||||||
newTeam++
|
newTeam++
|
||||||
if (newTeam > 2) {
|
if (newTeam > 2) {
|
||||||
newTeam = 0
|
newTeam = 0
|
||||||
|
|
@ -244,7 +258,7 @@ class StateMachineViewModel(context: Context) : ViewModel() {
|
||||||
parameterPacketGenerator.generatePacket(
|
parameterPacketGenerator.generatePacket(
|
||||||
targetAddress = device.address,
|
targetAddress = device.address,
|
||||||
subtype = 2, // Request Parameter Change
|
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
|
key2 = 4, value2 = gameCfg.maxHealth // Key 2 is Max Health
|
||||||
)
|
)
|
||||||
// If a device for some reason can't be configured (e.g. missing address),
|
// If a device for some reason can't be configured (e.g. missing address),
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="112" height="20" role="img" aria-label="status: OBSOLETE"><title>status: OBSOLETE</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="112" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="43" height="20" fill="#555"/><rect x="43" width="69" height="20" fill="#9f9f9f"/><rect width="112" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="225" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">status</text><text x="225" y="140" transform="scale(.1)" fill="#fff" textLength="330">status</text><text aria-hidden="true" x="765" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="590">OBSOLETE</text><text x="765" y="140" transform="scale(.1)" fill="#fff" textLength="590">OBSOLETE</text></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
Reference in a new issue