use std::collections::{BTreeMap, HashMap};
use yog_sync::{LazyLock, Mutex};
use jni::JNIEnv;
use jni::objects::{JByteArray, JClass, JString};
use jni::sys::jbyteArray;
use yog_abi::YogStr;
use yog_data::Schema;
use yog_data::with::Raw;
pub(crate) use yog_ids::yog::{DIMENSIONS, ENTITY_DATA};
struct Value {
bytes: Vec<u8>,
persist: bool,
}
type Values = BTreeMap<String, Value>;
type PendingChanges = BTreeMap<String, Option<Vec<u8>>>;
type EncodedValues = BTreeMap<String, Vec<u8>>;
static SERVER: LazyLock<Mutex<HashMap<String, Values>>> = LazyLock::new(Default::default);
static PENDING: LazyLock<Mutex<HashMap<String, PendingChanges>>> = LazyLock::new(Default::default);
static CLIENT: LazyLock<Mutex<HashMap<String, EncodedValues>>> = LazyLock::new(Default::default);
#[derive(Schema, Debug, Clone, Copy, PartialEq, Eq)]
enum Op {
Set,
Remove,
Clear,
}
#[derive(Schema, Debug, Clone, PartialEq)]
struct Change {
op: Op,
uuid: String,
key: String,
#[data(with = "Raw")]
value: Vec<u8>,
}
impl Change {
fn new(op: Op, uuid: &str, key: &str, value: &[u8]) -> Change {
Change {
op,
uuid: uuid.to_owned(),
key: key.to_owned(),
value: value.to_vec(),
}
}
}
fn encode(changes: &[Change]) -> Option<Vec<u8>> {
(!changes.is_empty()).then(|| yog_data::to_bytes(&changes.to_vec()))
}
fn decode(bytes: &[u8]) -> Vec<Change> {
yog_data::read_bytes::<Vec<Change>>(bytes).unwrap_or_default()
}
fn send_to_watchers(uuid: &str, bytes: &[u8]) {
unsafe {
let _: Option<bool> = bridge!(
"sendToTracking",
[
S(YogStr::from_str(uuid)),
S(YogStr::from_str(ENTITY_DATA)),
Bytes(bytes)
]
);
}
}
pub(crate) fn set(uuid: &str, key: &str, value: Option<&[u8]>, persist: bool) {
let changed = {
let mut store = SERVER.lock();
match value {
Some(v) => {
let values = store.entry(uuid.to_owned()).or_default();
let same = values
.get(key)
.is_some_and(|old| old.bytes == v && old.persist == persist);
values.insert(
key.to_owned(),
Value {
bytes: v.to_vec(),
persist,
},
);
!same
}
None => {
let removed = store
.get_mut(uuid)
.is_some_and(|values| values.remove(key).is_some());
if store.get(uuid).is_some_and(|v| v.is_empty()) {
store.remove(uuid);
}
removed
}
}
};
if changed {
PENDING
.lock()
.entry(uuid.to_owned())
.or_default()
.insert(key.to_owned(), value.map(<[u8]>::to_vec));
}
}
pub(crate) fn flush() {
let pending = std::mem::take(&mut *PENDING.lock());
for (uuid, keys) in pending {
let changes: Vec<Change> = keys
.into_iter()
.map(|(key, value)| match value {
Some(v) => Change::new(Op::Set, &uuid, &key, &v),
None => Change::new(Op::Remove, &uuid, &key, &[]),
})
.collect();
if let Some(bytes) = encode(&changes) {
send_to_watchers(&uuid, &bytes);
}
}
}
pub(crate) fn get(uuid: &str, key: &str) -> Option<Vec<u8>> {
SERVER.lock().get(uuid)?.get(key).map(|v| v.bytes.clone())
}
pub(crate) fn register() {
use yog_event::entity::{Data, DataValue, SetData, SetDimensions, VANILLA_SIZE};
crate::bus::serve::<Data>(|d| get(&d.uuid, &d.key).map(|bytes| DataValue { bytes }));
crate::bus::serve::<SetData>(|d| {
if d.uuid.is_empty() || d.key.is_empty() {
return false;
}
set(
&d.uuid,
&d.key,
d.value.as_ref().map(|v| v.bytes.as_slice()),
d.persist,
);
true
});
crate::bus::serve::<SetDimensions>(|d| {
let [w, h, eye] = d.size.unwrap_or(VANILLA_SIZE);
let found: bool = unsafe {
bridge!(
"entitySetDimensions",
[S(YogStr::from_str(&d.uuid)), F(w), F(h), F(eye)]
)
}
.unwrap_or(false);
if found {
let value = d.size.map(|[w, h, eye]| encode_dims(w, h, eye));
set(&d.uuid, DIMENSIONS, value.as_deref(), false);
}
found
});
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_yog_NativeBridge_nativeEntityDataFor<'l>(
mut env: JNIEnv<'l>,
_class: JClass<'l>,
uuid: JString<'l>,
) -> jbyteArray {
let uuid = jstr!(env, uuid, std::ptr::null_mut());
let mut changes = Vec::new();
if let Some(values) = SERVER.lock().get(&uuid) {
changes.push(Change::new(Op::Clear, &uuid, "", &[]));
changes.extend(
values
.iter()
.map(|(k, v)| Change::new(Op::Set, &uuid, k, &v.bytes)),
);
}
let Some(b) = encode(&changes) else {
return std::ptr::null_mut();
};
env.byte_array_from_slice(&b)
.map(|a| a.into_raw())
.unwrap_or(std::ptr::null_mut())
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_yog_NativeBridge_nativeEntityDataSave<'l>(
mut env: JNIEnv<'l>,
_class: JClass<'l>,
uuid: JString<'l>,
) -> jbyteArray {
let uuid = jstr!(env, uuid, std::ptr::null_mut());
let changes: Vec<Change> = SERVER
.lock()
.get(&uuid)
.into_iter()
.flat_map(|values| values.iter().filter(|(_, v)| v.persist))
.map(|(k, v)| Change::new(Op::Set, "", k, &v.bytes))
.collect();
let Some(b) = encode(&changes) else {
return std::ptr::null_mut();
};
env.byte_array_from_slice(&b)
.map(|a| a.into_raw())
.unwrap_or(std::ptr::null_mut())
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_yog_NativeBridge_nativeEntityDataLoad<'l>(
mut env: JNIEnv<'l>,
_class: JClass<'l>,
uuid: JString<'l>,
saved: JByteArray<'l>,
) {
let uuid = jstr!(env, uuid);
let Ok(saved) = env.convert_byte_array(&saved) else {
return;
};
let mut store = SERVER.lock();
let values = store.entry(uuid).or_default();
for r in decode(&saved) {
if r.op == Op::Set {
values.insert(
r.key,
Value {
bytes: r.value,
persist: true,
},
);
}
}
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_yog_NativeBridge_nativeEntityDataSet<'l>(
mut env: JNIEnv<'l>,
_class: JClass<'l>,
uuid: JString<'l>,
key: JString<'l>,
value: JByteArray<'l>,
) {
let uuid = jstr!(env, uuid);
let key = jstr!(env, key);
let value = if value.is_null() {
None
} else {
env.convert_byte_array(&value).ok()
};
if !uuid.is_empty() && !key.is_empty() {
set(&uuid, &key, value.as_deref(), false);
}
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_yog_NativeBridge_nativeEntityDataForget<'l>(
mut env: JNIEnv<'l>,
_class: JClass<'l>,
uuid: JString<'l>,
) {
let uuid = jstr!(env, uuid);
SERVER.lock().remove(&uuid);
}
pub(crate) fn server_stopped() {
SERVER.lock().clear();
PENDING.lock().clear();
}
pub(crate) fn receive(payload: &[u8]) {
let mut dims: Vec<(String, Option<[f32; 3]>)> = Vec::new();
{
let mut store = CLIENT.lock();
for r in decode(payload) {
match r.op {
Op::Set => {
if r.key == DIMENSIONS {
dims.push((r.uuid.clone(), decode_dims(&r.value)));
}
store.entry(r.uuid).or_default().insert(r.key, r.value);
}
Op::Remove => {
if r.key == DIMENSIONS {
dims.push((r.uuid.clone(), None));
}
if let Some(v) = store.get_mut(&r.uuid) {
v.remove(&r.key);
}
}
Op::Clear => {
let removed = store.remove(&r.uuid);
if removed.is_some_and(|v| v.contains_key(DIMENSIONS)) {
dims.push((r.uuid.clone(), None));
}
}
}
}
}
for (uuid, d) in dims {
let [w, h, e] = d.unwrap_or([-1.0, 0.0, 0.0]);
unsafe {
let _: Option<()> = host!(
"dev/yog/YogClientApi",
"setDimensions",
[S(YogStr::from_str(&uuid)), F(w), F(h), F(e)]
);
}
}
}
pub(crate) fn encode_dims(w: f32, h: f32, eye: f32) -> Vec<u8> {
yog_data::to_bytes(&[w, h, eye])
}
fn decode_dims(b: &[u8]) -> Option<[f32; 3]> {
yog_data::read_bytes(b).ok()
}
pub(crate) fn client_values(uuid: &str) -> Vec<(String, Vec<u8>)> {
CLIENT
.lock()
.get(uuid)
.map(|v| v.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default()
}
#[unsafe(no_mangle)]
pub extern "system" fn Java_dev_yog_NativeBridge_nativeEntityDataClientReset<'l>(
_env: JNIEnv<'l>,
_class: JClass<'l>,
) {
CLIENT.lock().clear();
}
pub(crate) fn client_get(uuid: &str, key: &str) -> Option<Vec<u8>> {
CLIENT.lock().get(uuid).and_then(|v| v.get(key)).cloned()
}
pub(crate) fn fill_frame(frame: &mut yog_body::Frame) {
frame.data = client_values(&frame.uuid)
.into_iter()
.map(|(key, value)| yog_body::wire::Kept { key, value })
.collect();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn changes_read_back() {
let changes = vec![
Change::new(Op::Set, "u", "k", &[1, 2, 3]),
Change::new(Op::Clear, "u", "", &[]),
];
assert_eq!(decode(&encode(&changes).unwrap()), changes);
assert_eq!(
decode_dims(&encode_dims(0.6, 1.8, 1.62)),
Some([0.6, 1.8, 1.62])
);
}
#[test]
fn frames_get_their_data() {
CLIENT
.lock()
.entry("ab".into())
.or_default()
.insert("m:k".into(), vec![9]);
let mut frame = yog_body::Frame {
uuid: "ab".into(),
..Default::default()
};
fill_frame(&mut frame);
assert_eq!(frame.data("m:k"), Some(&[9u8][..]));
}
}