mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-04-03 14:35:40 +02:00
Compare commits
4 Commits
aaa5c1088a
...
fix-crash-
| Author | SHA1 | Date | |
|---|---|---|---|
| ced79fefd2 | |||
| c7311a5966 | |||
|
|
071ea800ae | ||
| e01cc3ca11 |
67
.github/workflows/nightly-artifacts.yml
vendored
67
.github/workflows/nightly-artifacts.yml
vendored
@@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- test-mac
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "20 2 * * *"
|
- cron: "20 2 * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -152,3 +153,69 @@ jobs:
|
|||||||
name: linux-appimage
|
name: linux-appimage
|
||||||
path: dist/praktiKST-*-linux-x86_64.AppImage
|
path: dist/praktiKST-*-linux-x86_64.AppImage
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
|
build-macos-dmg:
|
||||||
|
name: Build macOS DMG (${{ matrix.os }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, macos-15-intel]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Resolve nightly version info
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')
|
||||||
|
SHORT_SHA="${GITHUB_SHA::7}"
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||||
|
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
|
||||||
|
echo "ASSET_BASENAME=praktiKST-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||||
|
echo "ARCH=$ARCH" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Set up Java 17
|
||||||
|
uses: actions/setup-java@v4.1.0
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: "17"
|
||||||
|
|
||||||
|
- name: Ensure mvnw is executable
|
||||||
|
run: chmod +x mvnw
|
||||||
|
|
||||||
|
- name: Build JAR and copy runtime dependencies
|
||||||
|
run: |
|
||||||
|
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||||
|
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||||
|
|
||||||
|
- name: Build macOS DMG with jpackage
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
jpackage \
|
||||||
|
--type dmg \
|
||||||
|
--name praktiKST \
|
||||||
|
--input target/dist-libs \
|
||||||
|
--main-jar app.jar \
|
||||||
|
--main-class kst4contest.view.Kst4ContestApplication \
|
||||||
|
--module-path target/dist-libs \
|
||||||
|
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
|
||||||
|
--dest dist
|
||||||
|
|
||||||
|
env:
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: "13.0"
|
||||||
|
|
||||||
|
- name: Rename DMG artifact
|
||||||
|
run: |
|
||||||
|
DMG=$(ls dist/*.dmg | head -n 1)
|
||||||
|
if [ -z "$DMG" ]; then
|
||||||
|
echo "No DMG produced by jpackage" && exit 1
|
||||||
|
fi
|
||||||
|
mv "$DMG" "dist/${ASSET_BASENAME}-macos-${ARCH}.dmg"
|
||||||
|
|
||||||
|
- name: Upload macOS artifact
|
||||||
|
uses: actions/upload-artifact@v4.3.4
|
||||||
|
with:
|
||||||
|
name: macos-dmg-${{ matrix.os }}
|
||||||
|
path: dist/praktiKST-*-macos-*.dmg
|
||||||
|
retention-days: 14
|
||||||
|
|||||||
65
.github/workflows/tagged-release.yml
vendored
65
.github/workflows/tagged-release.yml
vendored
@@ -134,6 +134,62 @@ jobs:
|
|||||||
name: linux-appimage
|
name: linux-appimage
|
||||||
path: dist/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage
|
path: dist/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||||
|
|
||||||
|
build-macos-dmg:
|
||||||
|
name: Build macOS DMG (${{ matrix.os }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, macos-15-intel]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4.1.7
|
||||||
|
|
||||||
|
- name: Set up Java 17
|
||||||
|
uses: actions/setup-java@v4.1.0
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: "17"
|
||||||
|
|
||||||
|
- name: Ensure mvnw is executable
|
||||||
|
run: chmod +x mvnw
|
||||||
|
|
||||||
|
- name: Build JAR and copy runtime dependencies
|
||||||
|
run: |
|
||||||
|
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||||
|
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||||
|
|
||||||
|
- name: Build macOS DMG with jpackage
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
jpackage \
|
||||||
|
--type dmg \
|
||||||
|
--name praktiKST \
|
||||||
|
--input target/dist-libs \
|
||||||
|
--main-jar app.jar \
|
||||||
|
--main-class kst4contest.view.Kst4ContestApplication \
|
||||||
|
--module-path target/dist-libs \
|
||||||
|
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
|
||||||
|
--dest dist
|
||||||
|
|
||||||
|
env:
|
||||||
|
MACOSX_DEPLOYMENT_TARGET: "13.0"
|
||||||
|
|
||||||
|
- name: Rename DMG artifact
|
||||||
|
run: |
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
DMG=$(ls dist/*.dmg | head -n 1)
|
||||||
|
if [ -z "$DMG" ]; then
|
||||||
|
echo "No DMG produced by jpackage" && exit 1
|
||||||
|
fi
|
||||||
|
mv "$DMG" "dist/praktiKST-${{ github.ref_name }}-macos-${ARCH}.dmg"
|
||||||
|
|
||||||
|
- name: Upload macOS artifact
|
||||||
|
uses: actions/upload-artifact@v4.3.4
|
||||||
|
with:
|
||||||
|
name: macos-dmg-${{ matrix.os }}
|
||||||
|
path: dist/praktiKST-${{ github.ref_name }}-macos-*.dmg
|
||||||
|
|
||||||
build-docs-pdf:
|
build-docs-pdf:
|
||||||
name: Build Documentation PDF
|
name: Build Documentation PDF
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -217,6 +273,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build-windows-zip
|
- build-windows-zip
|
||||||
- build-linux-appimage
|
- build-linux-appimage
|
||||||
|
- build-macos-dmg
|
||||||
- build-docs-pdf
|
- build-docs-pdf
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -232,6 +289,13 @@ jobs:
|
|||||||
name: linux-appimage
|
name: linux-appimage
|
||||||
path: release-assets/linux
|
path: release-assets/linux
|
||||||
|
|
||||||
|
- name: Download macOS artifacts
|
||||||
|
uses: actions/download-artifact@v4.1.3
|
||||||
|
with:
|
||||||
|
pattern: macos-dmg-*
|
||||||
|
merge-multiple: true
|
||||||
|
path: release-assets/macos
|
||||||
|
|
||||||
- name: Download PDF manuals
|
- name: Download PDF manuals
|
||||||
uses: actions/download-artifact@v4.1.3
|
uses: actions/download-artifact@v4.1.3
|
||||||
with:
|
with:
|
||||||
@@ -252,5 +316,6 @@ jobs:
|
|||||||
artifacts: >-
|
artifacts: >-
|
||||||
release-assets/windows/praktiKST-${{ github.ref_name }}-windows-x64.zip,
|
release-assets/windows/praktiKST-${{ github.ref_name }}-windows-x64.zip,
|
||||||
release-assets/linux/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage,
|
release-assets/linux/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage,
|
||||||
|
release-assets/macos/praktiKST-${{ github.ref_name }}-macos-*.dmg,
|
||||||
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-en.pdf,
|
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-en.pdf,
|
||||||
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-de.pdf
|
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-de.pdf
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
dr2x
|
|
||||||
oe3cin
|
|
||||||
15832
bugsept24.txt
15832
bugsept24.txt
File diff suppressed because it is too large
Load Diff
528
src/kstsimulator.py
Normal file
528
src/kstsimulator.py
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# KST-Server-Simulator / DO5AMF
|
||||||
|
# Usage: change configuration below and
|
||||||
|
# run. Enter 127.0.0.1 : 23001 as a
|
||||||
|
# target in KST4Contest or another
|
||||||
|
# KST chat client.
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# KONFIGURATION
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
PORT = 23001
|
||||||
|
HOST = '127.0.0.1'
|
||||||
|
|
||||||
|
MSG_TO_USER_INTERVAL = 300.0
|
||||||
|
LOGIN_LOGOUT_INTERVAL = 60.0
|
||||||
|
KEEP_ALIVE_INTERVAL = 10.0
|
||||||
|
CLIENT_WARMUP_TIME = 5.0
|
||||||
|
|
||||||
|
PROB_INACTIVE = 0.10
|
||||||
|
PROB_REACTIVE = 0.20
|
||||||
|
|
||||||
|
# QSY Wahrscheinlichkeit (Wie oft wechselt ein User seine Frequenz?)
|
||||||
|
# 0.05 = 5% Chance pro Nachricht, dass er die Frequenz ändert. Sonst bleibt er stabil.
|
||||||
|
PROB_QSY = 0.05
|
||||||
|
|
||||||
|
BANDS_VHF = { "2m": (144.150, 144.400), "70cm": (432.100, 432.300) }
|
||||||
|
BANDS_UHF = { "23cm": (1296.100, 1296.300), "3cm": (10368.100, 10368.250) }
|
||||||
|
|
||||||
|
CHANNELS_SETUP = {
|
||||||
|
"2": {
|
||||||
|
"NAME": "144/432 MHz",
|
||||||
|
"NUM_USERS": 777,
|
||||||
|
"BANDS": BANDS_VHF,
|
||||||
|
"RATES": {"PUBLIC": 0.5, "DIRECTED": 3.0},
|
||||||
|
"PERMANENT": [
|
||||||
|
{"call": "DK5EW", "name": "Erwin", "loc": "JN47NX"},
|
||||||
|
{"call": "DL1TEST", "name": "TestOp", "loc": "JO50XX"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"NAME": "Microwave",
|
||||||
|
"NUM_USERS": 333,
|
||||||
|
"BANDS": BANDS_UHF,
|
||||||
|
"RATES": {"PUBLIC": 0.2, "DIRECTED": 0.5},
|
||||||
|
"PERMANENT": [
|
||||||
|
{"call": "ON4KST", "name": "Alain", "loc": "JO20HI"},
|
||||||
|
{"call": "G4CBW", "name": "MwTest", "loc": "IO83AA"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
COUNTRY_MAPPING = {
|
||||||
|
"DL": ["JO", "JN"], "DA": ["JO", "JN"], "DF": ["JO", "JN"], "DJ": ["JO", "JN"], "DK": ["JO", "JN"], "DO": ["JO", "JN"],
|
||||||
|
"F": ["JN", "IN", "JO"], "G": ["IO", "JO"], "M": ["IO", "JO"], "2E": ["IO", "JO"],
|
||||||
|
"PA": ["JO"], "ON": ["JO"], "OZ": ["JO"], "SM": ["JO", "JP"], "LA": ["JO", "JP"],
|
||||||
|
"OH": ["KP"], "SP": ["JO", "KO"], "OK": ["JO", "JN"], "OM": ["JN", "KN"],
|
||||||
|
"HA": ["JN", "KN"], "S5": ["JN"], "9A": ["JN"], "HB9": ["JN"], "OE": ["JN"],
|
||||||
|
"I": ["JN", "JM"], "IK": ["JN", "JM"], "IU": ["JN", "JM"], "EA": ["IN", "IM"],
|
||||||
|
"CT": ["IM"], "EI": ["IO"], "GM": ["IO"], "GW": ["IO"], "YO": ["KN"],
|
||||||
|
"YU": ["KN"], "LZ": ["KN"], "SV": ["KM", "KN"], "UR": ["KO", "KN"],
|
||||||
|
"LY": ["KO"], "YL": ["KO"], "ES": ["KO"]
|
||||||
|
}
|
||||||
|
|
||||||
|
NAMES = ["Hans", "Peter", "Jo", "Alain", "Mike", "Sven", "Ole", "Jean", "Bob", "Tom", "Giovanni", "Mario", "Frank", "Steve", "Dave"]
|
||||||
|
|
||||||
|
MSG_TEMPLATES_WITH_FREQ = [
|
||||||
|
"QSY {freq}", "PSE QSY {freq}", "Calling CQ on {freq}", "I am QRV on {freq}",
|
||||||
|
"Listening on {freq}", "Can you try {freq}?", "Signals strong on {freq}",
|
||||||
|
"Scattering on {freq}", "Please go to {freq}", "Running test on {freq}",
|
||||||
|
"Any takers for {freq}?", "Back to {freq}", "QRG {freq}?", "Aircraft scatter {freq}"
|
||||||
|
]
|
||||||
|
|
||||||
|
MSG_TEMPLATES_TEXT_ONLY = [
|
||||||
|
"TNX for QSO", "73 all", "Anyone for sked?", "Good conditions",
|
||||||
|
"Nothing heard", "Rain scatter?", "Waiting for moonrise", "CQ Contest",
|
||||||
|
"QRZ?", "My locator is {loc}", "Band is open"
|
||||||
|
]
|
||||||
|
|
||||||
|
REPLY_TEMPLATES = [
|
||||||
|
"Hello {user}, 599 here", "Rgr {user}, tnx for report", "Yes {user}, QSY?",
|
||||||
|
"Sorry {user}, no copy", "Pse wait 5 min {user}", "Ok {user}, 73",
|
||||||
|
"Locator is {loc}", "Go to {freq} please", "Rgr {user}, gl"
|
||||||
|
]
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# CLIENT WRAPPER
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
class ConnectedClient:
|
||||||
|
def __init__(self, sock, addr):
|
||||||
|
self.sock = sock
|
||||||
|
self.addr = addr
|
||||||
|
self.call = f"GUEST_{random.randint(1000,9999)}"
|
||||||
|
self.channels = {"2"}
|
||||||
|
self.login_time = time.time()
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
def send_safe(self, data_str):
|
||||||
|
if not data_str: return True
|
||||||
|
with self.lock:
|
||||||
|
try:
|
||||||
|
self.sock.sendall(data_str.encode('latin-1', errors='replace'))
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
try: self.sock.close()
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# LOGIK KLASSEN
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
class MessageFactory:
|
||||||
|
@staticmethod
|
||||||
|
def get_stable_frequency(user, band_name, min_f, max_f):
|
||||||
|
"""Liefert eine stabile Frequenz für diesen User auf diesem Band"""
|
||||||
|
# Wenn noch keine Frequenz da ist ODER Zufall zuschlägt (QSY)
|
||||||
|
if band_name not in user['freqs'] or random.random() < PROB_QSY:
|
||||||
|
freq_val = round(random.uniform(min_f, max_f), 3)
|
||||||
|
user['freqs'][band_name] = f"{freq_val:.3f}"
|
||||||
|
|
||||||
|
return user['freqs'][band_name]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_chat_message(bands_config, user):
|
||||||
|
try:
|
||||||
|
# Entscheidung: Text mit Frequenz oder ohne?
|
||||||
|
if random.random() < 0.7:
|
||||||
|
# Wähle zufälliges Band aus den verfügbaren
|
||||||
|
band_name = random.choice(list(bands_config.keys()))
|
||||||
|
min_f, max_f = bands_config[band_name]
|
||||||
|
|
||||||
|
# Hole STABILE Frequenz für diesen User
|
||||||
|
freq_str = MessageFactory.get_stable_frequency(user, band_name, min_f, max_f)
|
||||||
|
|
||||||
|
return random.choice(MSG_TEMPLATES_WITH_FREQ).format(freq=freq_str)
|
||||||
|
else:
|
||||||
|
return random.choice(MSG_TEMPLATES_TEXT_ONLY).format(loc=user['loc'])
|
||||||
|
except: return "TNX 73"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_reply_msg(bands, target_call, my_loc):
|
||||||
|
try:
|
||||||
|
tmpl = random.choice(REPLY_TEMPLATES)
|
||||||
|
freq_str = "QSY?"
|
||||||
|
# Bei Replies simulieren wir oft nur "QSY?" ohne konkrete Frequenz,
|
||||||
|
# oder nutzen eine zufällige, da der Kontext fehlt.
|
||||||
|
if "{freq}" in tmpl and bands:
|
||||||
|
band_name = random.choice(list(bands.keys()))
|
||||||
|
min_f, max_f = bands[band_name]
|
||||||
|
freq_str = f"{round(random.uniform(min_f, max_f), 3):.3f}"
|
||||||
|
return tmpl.format(user=target_call, loc=my_loc, freq=freq_str)
|
||||||
|
except: return "TNX 73"
|
||||||
|
|
||||||
|
class UserFactory:
|
||||||
|
registry = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_create_user(cls, channel_id, current_channel_users):
|
||||||
|
# 1. Reuse existing
|
||||||
|
candidates = [u for call, u in cls.registry.items() if call not in current_channel_users]
|
||||||
|
if candidates and random.random() < 0.5:
|
||||||
|
return random.choice(candidates)
|
||||||
|
|
||||||
|
# 2. Create new
|
||||||
|
return cls._create_new_unique_user(channel_id, current_channel_users)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_new_unique_user(cls, channel_id, current_channel_users):
|
||||||
|
while True:
|
||||||
|
prefix = random.choice(list(COUNTRY_MAPPING.keys()))
|
||||||
|
num = random.randint(0, 9)
|
||||||
|
suffix = "".join(random.choices("ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=random.randint(1,3)))
|
||||||
|
call = f"{prefix}{num}{suffix}"
|
||||||
|
|
||||||
|
if call in current_channel_users: continue
|
||||||
|
if call in cls.registry: return cls.registry[call]
|
||||||
|
|
||||||
|
valid_grids = COUNTRY_MAPPING[prefix]
|
||||||
|
grid_prefix = random.choice(valid_grids)
|
||||||
|
sq_num = f"{random.randint(0,99):02d}"
|
||||||
|
sub = "".join(random.choices("ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=2))
|
||||||
|
loc = f"{grid_prefix}{sq_num}{sub}"
|
||||||
|
|
||||||
|
name = random.choice(NAMES)
|
||||||
|
rand = random.random()
|
||||||
|
if rand < PROB_INACTIVE: role = "INACTIVE"
|
||||||
|
elif rand < (PROB_INACTIVE + PROB_REACTIVE): role = "REACTIVE"
|
||||||
|
else: role = "ACTIVE"
|
||||||
|
|
||||||
|
# Neu V31: Frequenz-Gedächtnis
|
||||||
|
user_data = {
|
||||||
|
"call": call,
|
||||||
|
"name": name,
|
||||||
|
"loc": loc,
|
||||||
|
"role": role,
|
||||||
|
"freqs": {} # Speicher für { '2m': '144.300' }
|
||||||
|
}
|
||||||
|
|
||||||
|
cls.registry[call] = user_data
|
||||||
|
return user_data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_permanent(cls, user_data):
|
||||||
|
# Sicherstellen, dass auch Permanent User Freq-Memory haben
|
||||||
|
if "freqs" not in user_data:
|
||||||
|
user_data["freqs"] = {}
|
||||||
|
cls.registry[user_data['call']] = user_data
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# CHANNEL INSTANCE
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
class ChannelInstance:
|
||||||
|
def __init__(self, cid, config, server):
|
||||||
|
self.id = cid
|
||||||
|
self.config = config
|
||||||
|
self.server = server
|
||||||
|
|
||||||
|
self.users_pool = []
|
||||||
|
self.online_users = {}
|
||||||
|
self.history_chat = []
|
||||||
|
|
||||||
|
self.last_pub = time.time()
|
||||||
|
self.last_dir = time.time()
|
||||||
|
self.last_me = time.time()
|
||||||
|
self.last_login = time.time()
|
||||||
|
|
||||||
|
self.rate_pub = 1.0 / config["RATES"]["PUBLIC"]
|
||||||
|
self.rate_dir = 1.0 / config["RATES"]["DIRECTED"]
|
||||||
|
|
||||||
|
self._init_data()
|
||||||
|
|
||||||
|
def _init_data(self):
|
||||||
|
print(f"[*] Init Channel {self.id} ({self.config['NAME']})...")
|
||||||
|
|
||||||
|
for u in self.config["PERMANENT"]:
|
||||||
|
u_full = u.copy()
|
||||||
|
u_full["role"] = "ACTIVE"
|
||||||
|
UserFactory.register_permanent(u_full)
|
||||||
|
self.online_users[u['call']] = u_full
|
||||||
|
|
||||||
|
for _ in range(self.config["NUM_USERS"]):
|
||||||
|
new_u = UserFactory.get_or_create_user(self.id, self.online_users.keys())
|
||||||
|
self.users_pool.append(new_u)
|
||||||
|
|
||||||
|
fill = int(self.config["NUM_USERS"] * 0.9)
|
||||||
|
for i in range(fill):
|
||||||
|
u = self.users_pool[i]
|
||||||
|
if u['call'] not in self.online_users:
|
||||||
|
self.online_users[u['call']] = u
|
||||||
|
|
||||||
|
print(f"[*] Channel {self.id} ready: {len(self.online_users)} Users.")
|
||||||
|
self._prefill_history()
|
||||||
|
|
||||||
|
def _prefill_history(self):
|
||||||
|
actives = [u for u in self.online_users.values() if u['role'] == "ACTIVE"]
|
||||||
|
if not actives: return
|
||||||
|
start = datetime.now() - timedelta(minutes=15)
|
||||||
|
for i in range(30):
|
||||||
|
msg_time = start + timedelta(seconds=i*30)
|
||||||
|
ts = str(int(msg_time.timestamp()))
|
||||||
|
sender = random.choice(actives)
|
||||||
|
if i % 2 == 0:
|
||||||
|
text = MessageFactory.get_chat_message(self.config["BANDS"], sender)
|
||||||
|
frame = f"CH|{self.id}|{ts}|{sender['call']}|{sender['name']}|0|{text}|0|\r\n"
|
||||||
|
else:
|
||||||
|
target = random.choice(list(self.online_users.values()))
|
||||||
|
text = MessageFactory.get_reply_msg(self.config["BANDS"], target['call'], sender['loc'])
|
||||||
|
frame = f"CH|{self.id}|{ts}|{sender['call']}|{sender['name']}|0|{text}|{target['call']}|\r\n"
|
||||||
|
self.history_chat.append(frame)
|
||||||
|
|
||||||
|
def tick(self, now):
|
||||||
|
actives = [u for u in self.online_users.values() if u['role'] == "ACTIVE"]
|
||||||
|
if not actives: return
|
||||||
|
|
||||||
|
# PUBLIC
|
||||||
|
if now - self.last_pub > self.rate_pub:
|
||||||
|
self.last_pub = now
|
||||||
|
u = random.choice(actives)
|
||||||
|
# V31: Nutzt jetzt get_chat_message, das das Freq-Memory abfragt
|
||||||
|
text = MessageFactory.get_chat_message(self.config["BANDS"], u)
|
||||||
|
ts = str(int(now))
|
||||||
|
frame = f"CH|{self.id}|{ts}|{u['call']}|{u['name']}|0|{text}|0|\r\n"
|
||||||
|
self._add_hist(frame)
|
||||||
|
self.server.broadcast_to_channel(self.id, frame)
|
||||||
|
|
||||||
|
# DIRECTED
|
||||||
|
if now - self.last_dir > self.rate_dir:
|
||||||
|
self.last_dir = now
|
||||||
|
if len(actives) > 5:
|
||||||
|
u1 = random.choice(actives)
|
||||||
|
u2 = random.choice(list(self.online_users.values()))
|
||||||
|
if u1 != u2:
|
||||||
|
if random.random() < 0.5:
|
||||||
|
# Auch hier Frequenzstabilität beachten
|
||||||
|
text = MessageFactory.get_chat_message(self.config["BANDS"], u1)
|
||||||
|
else:
|
||||||
|
text = MessageFactory.get_reply_msg(self.config["BANDS"], u2['call'], u1['loc'])
|
||||||
|
ts = str(int(now))
|
||||||
|
frame = f"CH|{self.id}|{ts}|{u1['call']}|{u1['name']}|0|{text}|{u2['call']}|\r\n"
|
||||||
|
self.server.broadcast_to_channel(self.id, frame)
|
||||||
|
if u2['role'] != "INACTIVE":
|
||||||
|
threading.Thread(target=self._schedule_reply, args=(u2['call'], u1['call']), daemon=True).start()
|
||||||
|
|
||||||
|
# MSG TO YOU
|
||||||
|
if now - self.last_me > MSG_TO_USER_INTERVAL:
|
||||||
|
self.last_me = now
|
||||||
|
target_client = self.server.get_random_subscriber(self.id)
|
||||||
|
if target_client and actives:
|
||||||
|
if not target_client.call.startswith("GUEST"):
|
||||||
|
sender = random.choice(actives)
|
||||||
|
text = MessageFactory.get_chat_message(self.config["BANDS"], sender)
|
||||||
|
print(f"[SIM Ch{self.id}] MSG TO YOU ({target_client.call})")
|
||||||
|
self.process_msg(sender['call'], sender['name'], text, target_client.call)
|
||||||
|
|
||||||
|
# LOGIN/LOGOUT
|
||||||
|
if now - self.last_login > LOGIN_LOGOUT_INTERVAL:
|
||||||
|
self.last_login = now
|
||||||
|
if random.choice(['IN', 'OUT']) == 'OUT' and len(self.online_users) > 20:
|
||||||
|
cands = [c for c in self.online_users if c not in [p['call'] for p in self.config["PERMANENT"]]]
|
||||||
|
if cands:
|
||||||
|
l = random.choice(cands)
|
||||||
|
del self.online_users[l]
|
||||||
|
self.server.broadcast_to_channel(self.id, f"UR6|{self.id}|{l}|\r\n")
|
||||||
|
else:
|
||||||
|
candidates = [u for u in self.users_pool if u['call'] not in self.online_users]
|
||||||
|
if candidates:
|
||||||
|
n = random.choice(candidates)
|
||||||
|
self.online_users[n['call']] = n
|
||||||
|
self.server.broadcast_to_channel(self.id, f"UA5|{self.id}|{n['call']}|{n['name']}|{n['loc']}|2|\r\n")
|
||||||
|
|
||||||
|
def process_msg(self, sender, name, text, target):
|
||||||
|
ts = str(int(time.time()))
|
||||||
|
frame = f"CH|{self.id}|{ts}|{sender}|{name}|0|{text}|{target}|\r\n"
|
||||||
|
if target == "0": self._add_hist(frame)
|
||||||
|
self.server.broadcast_to_channel(self.id, frame)
|
||||||
|
if target in self.online_users:
|
||||||
|
threading.Thread(target=self._schedule_reply, args=(target, sender), daemon=True).start()
|
||||||
|
|
||||||
|
def _schedule_reply(self, sim_sender, real_target):
|
||||||
|
if sim_sender not in self.online_users: return
|
||||||
|
u = self.online_users[sim_sender]
|
||||||
|
if u['role'] == "INACTIVE": return
|
||||||
|
|
||||||
|
time.sleep(random.uniform(2.0, 5.0))
|
||||||
|
if sim_sender in self.online_users:
|
||||||
|
text = MessageFactory.get_reply_msg(self.config["BANDS"], real_target, u['loc'])
|
||||||
|
ts = str(int(time.time()))
|
||||||
|
|
||||||
|
if self.server.is_real_user(real_target):
|
||||||
|
print(f"[REPLY Ch{self.id}] {sim_sender} -> {real_target}")
|
||||||
|
|
||||||
|
frame = f"CH|{self.id}|{ts}|{sim_sender}|{u['name']}|0|{text}|{real_target}|\r\n"
|
||||||
|
self.server.broadcast_to_channel(self.id, frame)
|
||||||
|
|
||||||
|
def _add_hist(self, frame):
|
||||||
|
self.history_chat.append(frame)
|
||||||
|
if len(self.history_chat) > 50: self.history_chat.pop(0)
|
||||||
|
|
||||||
|
def get_full_init_blob(self):
|
||||||
|
blob = ""
|
||||||
|
for u in self.online_users.values():
|
||||||
|
blob += f"UA0|{self.id}|{u['call']}|{u['name']}|{u['loc']}|0|\r\n"
|
||||||
|
for h in self.history_chat: blob += h
|
||||||
|
blob += f"UE|{self.id}|{len(self.online_users)}|\r\n"
|
||||||
|
return blob.encode('latin-1', errors='replace')
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# SERVER
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
class KSTServerV31:
|
||||||
|
def __init__(self):
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.running = True
|
||||||
|
self.clients = {}
|
||||||
|
self.channels = {}
|
||||||
|
|
||||||
|
for cid, cfg in CHANNELS_SETUP.items():
|
||||||
|
self.channels[cid] = ChannelInstance(cid, cfg, self)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
threading.Thread(target=self._sim_loop, daemon=True).start()
|
||||||
|
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
try:
|
||||||
|
s.bind((HOST, PORT))
|
||||||
|
s.listen(5)
|
||||||
|
s.settimeout(1.0)
|
||||||
|
print(f"[*] ON4KST V31 (Stable Frequencies) running on {HOST}:{PORT}")
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
sock, addr = s.accept()
|
||||||
|
print(f"[*] CONNECT: {addr}")
|
||||||
|
threading.Thread(target=self._handle_client, args=(sock,), daemon=True).start()
|
||||||
|
except socket.timeout: continue
|
||||||
|
except OSError: break
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n[!] Stop.")
|
||||||
|
finally:
|
||||||
|
self.running = False
|
||||||
|
try: s.close()
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
def _handle_client(self, sock):
|
||||||
|
client_obj = ConnectedClient(sock, None)
|
||||||
|
with self.lock:
|
||||||
|
self.clients[sock] = client_obj
|
||||||
|
|
||||||
|
buffer = ""
|
||||||
|
try:
|
||||||
|
while self.running:
|
||||||
|
try: data = sock.recv(2048)
|
||||||
|
except: break
|
||||||
|
if not data: break
|
||||||
|
|
||||||
|
buffer += data.decode('latin-1', errors='replace')
|
||||||
|
while '\n' in buffer:
|
||||||
|
line, buffer = buffer.split('\n', 1)
|
||||||
|
line = line.strip()
|
||||||
|
if not line: continue
|
||||||
|
|
||||||
|
parts = line.split('|')
|
||||||
|
cmd = parts[0]
|
||||||
|
|
||||||
|
if cmd == 'LOGIN' or cmd == 'LOGINC':
|
||||||
|
if len(parts) > 1:
|
||||||
|
client_obj.call = parts[1].strip().upper()
|
||||||
|
print(f"[LOGIN] {client_obj.call} (Ch 2)")
|
||||||
|
|
||||||
|
client_obj.send_safe(f"LOGSTAT|100|2|PySimV31|KEY|Conf|3|\r\n")
|
||||||
|
if cmd == 'LOGIN':
|
||||||
|
self._send_channel_init(client_obj, "2")
|
||||||
|
|
||||||
|
elif cmd == 'SDONE':
|
||||||
|
self._send_channel_init(client_obj, "2")
|
||||||
|
|
||||||
|
elif cmd.startswith('ACHAT'):
|
||||||
|
if len(parts) >= 2:
|
||||||
|
new_chan = parts[1]
|
||||||
|
if new_chan in self.channels:
|
||||||
|
client_obj.channels.add(new_chan)
|
||||||
|
print(f"[ACHAT] {client_obj.call} -> Ch {new_chan}")
|
||||||
|
self._send_channel_init(client_obj, new_chan)
|
||||||
|
|
||||||
|
elif cmd == 'MSG':
|
||||||
|
if len(parts) >= 4:
|
||||||
|
cid = parts[1]
|
||||||
|
target = parts[2]
|
||||||
|
text = parts[3]
|
||||||
|
if text.lower().startswith("/cq"):
|
||||||
|
spl = text.split(' ', 2)
|
||||||
|
if len(spl) >= 3:
|
||||||
|
target = spl[1]; text = spl[2]
|
||||||
|
if cid in self.channels:
|
||||||
|
self.channels[cid].process_msg(client_obj.call, "Me", text, target)
|
||||||
|
|
||||||
|
elif cmd == 'CK': pass
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[!] Err: {e}")
|
||||||
|
finally:
|
||||||
|
with self.lock:
|
||||||
|
if sock in self.clients: del self.clients[sock]
|
||||||
|
client_obj.close()
|
||||||
|
|
||||||
|
def _send_channel_init(self, client_obj, cid):
|
||||||
|
if cid in self.channels:
|
||||||
|
full_blob = self.channels[cid].get_full_init_blob()
|
||||||
|
client_obj.send_safe(full_blob.decode('latin-1'))
|
||||||
|
|
||||||
|
def broadcast_to_channel(self, cid, frame):
|
||||||
|
now = time.time()
|
||||||
|
with self.lock:
|
||||||
|
targets = list(self.clients.values())
|
||||||
|
|
||||||
|
for c in targets:
|
||||||
|
if cid in c.channels:
|
||||||
|
if now - c.login_time > CLIENT_WARMUP_TIME:
|
||||||
|
c.send_safe(frame)
|
||||||
|
|
||||||
|
def get_random_subscriber(self, cid):
|
||||||
|
with self.lock:
|
||||||
|
subs = [c for c in self.clients.values() if cid in c.channels and not c.call.startswith("GUEST")]
|
||||||
|
return random.choice(subs) if subs else None
|
||||||
|
|
||||||
|
def is_real_user(self, call):
|
||||||
|
with self.lock:
|
||||||
|
for c in self.clients.values():
|
||||||
|
if c.call.upper() == call.upper() and not c.call.startswith("GUEST"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _sim_loop(self):
|
||||||
|
print("[*] Sim Loop running...")
|
||||||
|
last_ka = time.time()
|
||||||
|
while self.running:
|
||||||
|
now = time.time()
|
||||||
|
time.sleep(0.02)
|
||||||
|
|
||||||
|
for c in self.channels.values():
|
||||||
|
c.tick(now)
|
||||||
|
|
||||||
|
if now - last_ka > KEEP_ALIVE_INTERVAL:
|
||||||
|
last_ka = now
|
||||||
|
self.broadcast_global("CK|\r\n")
|
||||||
|
|
||||||
|
def broadcast_global(self, frame):
|
||||||
|
with self.lock:
|
||||||
|
targets = list(self.clients.values())
|
||||||
|
for c in targets:
|
||||||
|
c.send_safe(frame)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
KSTServerV31().start()
|
||||||
@@ -23,6 +23,7 @@ import kst4contest.locatorUtils.DirectionUtils;
|
|||||||
import kst4contest.logic.PriorityCalculator;
|
import kst4contest.logic.PriorityCalculator;
|
||||||
import kst4contest.model.*;
|
import kst4contest.model.*;
|
||||||
import kst4contest.test.MockKstServer;
|
import kst4contest.test.MockKstServer;
|
||||||
|
import kst4contest.utils.BoundedDequeObservableList;
|
||||||
import kst4contest.utils.PlayAudioUtils;
|
import kst4contest.utils.PlayAudioUtils;
|
||||||
import kst4contest.view.Kst4ContestApplication;
|
import kst4contest.view.Kst4ContestApplication;
|
||||||
|
|
||||||
@@ -1007,7 +1008,8 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
// ******All abstract types below here are used by the messageprocessor!
|
// ******All abstract types below here are used by the messageprocessor!
|
||||||
// ***************
|
// ***************
|
||||||
|
|
||||||
private ObservableList<ChatMessage> lst_globalChatMessageList = FXCollections.observableArrayList(); //All chatmessages will be put in there, later create filtered message lists
|
private static final int MAX_CHAT_MESSAGES = 10000;
|
||||||
|
private final BoundedDequeObservableList<ChatMessage> lst_globalChatMessageList = new BoundedDequeObservableList<>(MAX_CHAT_MESSAGES); //All chatmessages will be put in there, later create filtered message lists
|
||||||
// private ObservableList<ChatMessage> lst_toAllMessageList = FXCollections.observableArrayList(); // directed to all
|
// private ObservableList<ChatMessage> lst_toAllMessageList = FXCollections.observableArrayList(); // directed to all
|
||||||
// (beacon)
|
// (beacon)
|
||||||
private FilteredList<ChatMessage> lst_toAllMessageList = new FilteredList<>(lst_globalChatMessageList); // directed to all
|
private FilteredList<ChatMessage> lst_toAllMessageList = new FilteredList<>(lst_globalChatMessageList); // directed to all
|
||||||
@@ -1152,13 +1154,14 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
this.lst_selectedCallSignInfofilteredMessageList = lst_selectedCallSignInfofilteredMessageList;
|
this.lst_selectedCallSignInfofilteredMessageList = lst_selectedCallSignInfofilteredMessageList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addChatMessage(ChatMessage message) {
|
||||||
|
lst_globalChatMessageList.addFirst(message);
|
||||||
|
}
|
||||||
|
|
||||||
public ObservableList<ChatMessage> getLst_globalChatMessageList() {
|
public ObservableList<ChatMessage> getLst_globalChatMessageList() {
|
||||||
return lst_globalChatMessageList;
|
return lst_globalChatMessageList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLst_globalChatMessageList(ObservableList<ChatMessage> lst_globalChatMessageList) {
|
|
||||||
this.lst_globalChatMessageList = lst_globalChatMessageList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getHostname() {
|
public String getHostname() {
|
||||||
return hostname;
|
return hostname;
|
||||||
|
|||||||
@@ -772,7 +772,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
dummy.setCallSign("ALL");
|
dummy.setCallSign("ALL");
|
||||||
newMessageArrived.setReceiver(dummy);
|
newMessageArrived.setReceiver(dummy);
|
||||||
|
|
||||||
this.client.getLst_globalChatMessageList().add(0, newMessageArrived); // sdtout to all message-List
|
this.client.addChatMessage(newMessageArrived); // sdtout to all message-List
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
//message is directed to another chatmember, process as such!
|
//message is directed to another chatmember, process as such!
|
||||||
@@ -817,7 +817,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
if (newMessageArrived.getReceiver().getCallSign()
|
if (newMessageArrived.getReceiver().getCallSign()
|
||||||
.equals(this.client.getChatPreferences().getStn_loginCallSign())) {
|
.equals(this.client.getChatPreferences().getStn_loginCallSign())) {
|
||||||
|
|
||||||
this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
|
this.client.addChatMessage(newMessageArrived);
|
||||||
|
|
||||||
if (this.client.getChatPreferences().isNotify_playSimpleSounds()) {
|
if (this.client.getChatPreferences().isNotify_playSimpleSounds()) {
|
||||||
this.client.getPlayAudioUtils().playNoiseLauncher('P');
|
this.client.getPlayAudioUtils().playNoiseLauncher('P');
|
||||||
@@ -960,7 +960,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
String originalMessage = newMessageArrived.getMessageText();
|
String originalMessage = newMessageArrived.getMessageText();
|
||||||
newMessageArrived
|
newMessageArrived
|
||||||
.setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage);
|
.setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage);
|
||||||
this.client.getLst_globalChatMessageList().add(0,newMessageArrived);
|
this.client.addChatMessage(newMessageArrived);
|
||||||
|
|
||||||
// if you sent the message to another station, it will be sorted in to
|
// if you sent the message to another station, it will be sorted in to
|
||||||
// the "to me message list" with modified messagetext, added rxers callsign
|
// the "to me message list" with modified messagetext, added rxers callsign
|
||||||
@@ -1024,7 +1024,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
newMessageArrived.getSender().setInAngleAndRange(false);
|
newMessageArrived.getSender().setInAngleAndRange(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
|
this.client.addChatMessage(newMessageArrived);
|
||||||
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
|
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
|
||||||
}
|
}
|
||||||
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
|
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
|
||||||
@@ -1364,7 +1364,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
dummy.setCallSign("ALL");
|
dummy.setCallSign("ALL");
|
||||||
newMessageArrived.setReceiver(dummy);
|
newMessageArrived.setReceiver(dummy);
|
||||||
|
|
||||||
this.client.getLst_globalChatMessageList().add(0, newMessageArrived); // sdtout to all message-List
|
this.client.addChatMessage(newMessageArrived); // sdtout to all message-List
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
//message is directed to another chatmember, process as such!
|
//message is directed to another chatmember, process as such!
|
||||||
@@ -1408,7 +1408,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
if (newMessageArrived.getReceiver().getCallSign()
|
if (newMessageArrived.getReceiver().getCallSign()
|
||||||
.equals(this.client.getChatPreferences().getStn_loginCallSign())) {
|
.equals(this.client.getChatPreferences().getStn_loginCallSign())) {
|
||||||
|
|
||||||
this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
|
this.client.addChatMessage(newMessageArrived);
|
||||||
|
|
||||||
System.out.println("Historic message directed to me: " + newMessageArrived.getReceiver().getCallSign() + ".");
|
System.out.println("Historic message directed to me: " + newMessageArrived.getReceiver().getCallSign() + ".");
|
||||||
|
|
||||||
@@ -1421,7 +1421,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
String originalMessage = newMessageArrived.getMessageText();
|
String originalMessage = newMessageArrived.getMessageText();
|
||||||
newMessageArrived
|
newMessageArrived
|
||||||
.setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage);
|
.setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage);
|
||||||
this.client.getLst_globalChatMessageList().add(0,newMessageArrived);
|
this.client.addChatMessage(newMessageArrived);
|
||||||
|
|
||||||
// if you sent the message to another station, it will be sorted in to
|
// if you sent the message to another station, it will be sorted in to
|
||||||
// the "to me message list" with modified messagetext, added rxers callsign
|
// the "to me message list" with modified messagetext, added rxers callsign
|
||||||
@@ -1441,7 +1441,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
newMessageArrived.getSender().setInAngleAndRange(false);
|
newMessageArrived.getSender().setInAngleAndRange(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
|
this.client.addChatMessage(newMessageArrived);
|
||||||
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
|
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
|
||||||
}
|
}
|
||||||
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
|
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
|
||||||
@@ -1514,7 +1514,7 @@ public class MessageBusManagementThread extends Thread {
|
|||||||
|
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
client.getLst_globalChatMessageList().add(pwErrorMsg);
|
client.addChatMessage(pwErrorMsg);
|
||||||
// client.getLst_toMeMessageList().add(pwErrorMsg);
|
// client.getLst_toMeMessageList().add(pwErrorMsg);
|
||||||
// client.getLst_toAllMessageList().add(pwErrorMsg);
|
// client.getLst_toAllMessageList().add(pwErrorMsg);
|
||||||
}
|
}
|
||||||
|
|||||||
168
src/main/java/kst4contest/utils/BoundedDequeObservableList.java
Normal file
168
src/main/java/kst4contest/utils/BoundedDequeObservableList.java
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
package kst4contest.utils;
|
||||||
|
|
||||||
|
import javafx.collections.ObservableListBase;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bounded ObservableList backed by a circular buffer (ring buffer).
|
||||||
|
* <p>
|
||||||
|
* Provides O(1) {@link #addFirst} and {@link #addLast} as well as O(1)
|
||||||
|
* random access via {@link #get}. When the list reaches {@code maxCapacity},
|
||||||
|
* adding a new element at the front automatically evicts the oldest element
|
||||||
|
* at the back — and vice versa.
|
||||||
|
* <p>
|
||||||
|
* This is a drop-in replacement for {@code FXCollections.observableArrayList()}
|
||||||
|
* wherever elements are prepended frequently, e.g. chat message lists.
|
||||||
|
*/
|
||||||
|
public class BoundedDequeObservableList<E> extends ObservableListBase<E> {
|
||||||
|
|
||||||
|
private final int maxCapacity;
|
||||||
|
private final Object[] elements;
|
||||||
|
private int head = 0;
|
||||||
|
private int size = 0;
|
||||||
|
|
||||||
|
public BoundedDequeObservableList(int maxCapacity) {
|
||||||
|
if (maxCapacity <= 0) throw new IllegalArgumentException("maxCapacity must be > 0");
|
||||||
|
this.maxCapacity = maxCapacity;
|
||||||
|
this.elements = new Object[maxCapacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── read access ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int size() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public E get(int index) {
|
||||||
|
checkIndex(index);
|
||||||
|
return (E) elements[physicalIndex(index)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── O(1) deque operations ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts {@code element} at index 0 (newest-first order).
|
||||||
|
* If the list is already at capacity the oldest element (last index) is
|
||||||
|
* removed first — both changes are reported as a single compound change.
|
||||||
|
*/
|
||||||
|
public void addFirst(E element) {
|
||||||
|
beginChange();
|
||||||
|
if (size == maxCapacity) {
|
||||||
|
// evict last element
|
||||||
|
int lastPhysical = physicalIndex(size - 1);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
E evicted = (E) elements[lastPhysical];
|
||||||
|
elements[lastPhysical] = null;
|
||||||
|
size--;
|
||||||
|
nextRemove(size, evicted); // index after decrement == old last index
|
||||||
|
}
|
||||||
|
head = (head - 1 + maxCapacity) % maxCapacity;
|
||||||
|
elements[head] = element;
|
||||||
|
size++;
|
||||||
|
nextAdd(0, 1);
|
||||||
|
endChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends {@code element} at the last index (oldest-first order).
|
||||||
|
* If the list is already at capacity the newest element (index 0) is
|
||||||
|
* removed first.
|
||||||
|
*/
|
||||||
|
public void addLast(E element) {
|
||||||
|
beginChange();
|
||||||
|
if (size == maxCapacity) {
|
||||||
|
// evict first element
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
E evicted = (E) elements[head];
|
||||||
|
elements[head] = null;
|
||||||
|
head = (head + 1) % maxCapacity;
|
||||||
|
size--;
|
||||||
|
nextRemove(0, evicted);
|
||||||
|
}
|
||||||
|
elements[physicalIndex(size)] = element;
|
||||||
|
size++;
|
||||||
|
nextAdd(size - 1, size);
|
||||||
|
endChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── standard List mutation (O(n) — use addFirst/addLast for hot path) ─────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void add(int index, E element) {
|
||||||
|
if (index == 0) {
|
||||||
|
addFirst(element);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (index == size) {
|
||||||
|
addLast(element);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
checkIndexForAdd(index);
|
||||||
|
beginChange();
|
||||||
|
if (size == maxCapacity) {
|
||||||
|
int lastPhysical = physicalIndex(size - 1);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
E evicted = (E) elements[lastPhysical];
|
||||||
|
elements[lastPhysical] = null;
|
||||||
|
size--;
|
||||||
|
nextRemove(size, evicted);
|
||||||
|
}
|
||||||
|
// shift elements [index .. size-1] one position towards the end
|
||||||
|
for (int i = size; i > index; i--) {
|
||||||
|
elements[physicalIndex(i)] = elements[physicalIndex(i - 1)];
|
||||||
|
}
|
||||||
|
elements[physicalIndex(index)] = element;
|
||||||
|
size++;
|
||||||
|
nextAdd(index, index + 1);
|
||||||
|
endChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public E remove(int index) {
|
||||||
|
checkIndex(index);
|
||||||
|
beginChange();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
E removed = (E) elements[physicalIndex(index)];
|
||||||
|
// shift elements [index+1 .. size-1] one position towards the front
|
||||||
|
for (int i = index; i < size - 1; i++) {
|
||||||
|
elements[physicalIndex(i)] = elements[physicalIndex(i + 1)];
|
||||||
|
}
|
||||||
|
elements[physicalIndex(size - 1)] = null;
|
||||||
|
size--;
|
||||||
|
nextRemove(index, removed);
|
||||||
|
endChange();
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public E set(int index, E element) {
|
||||||
|
checkIndex(index);
|
||||||
|
beginChange();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
E old = (E) elements[physicalIndex(index)];
|
||||||
|
elements[physicalIndex(index)] = element;
|
||||||
|
nextSet(index, old);
|
||||||
|
endChange();
|
||||||
|
return old;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private int physicalIndex(int virtualIndex) {
|
||||||
|
return (head + virtualIndex) % maxCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkIndex(int index) {
|
||||||
|
if (index < 0 || index >= size)
|
||||||
|
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkIndexForAdd(int index) {
|
||||||
|
if (index < 0 || index > size)
|
||||||
|
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
|
||||||
|
}
|
||||||
|
}
|
||||||
1783
udpReaderBackup.txt
1783
udpReaderBackup.txt
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user