Skip to main content

yog_event/planes/system/
java.rs

1//! The bus's messages for Java: a record per event (`dev.yog.bus.YogEvents`)
2//! and per service (`YogServices`), from the same schemas, that encodes
3//! itself and goes on the bus — the host fires events and calls services
4//! through these instead of a JNI method each, and reads the replies.
5
6mod events;
7mod frames;
8mod mirrors;
9mod services;
10mod types;
11mod views;
12mod words;
13
14use std::fmt::Write;
15
16use yog_codegen::{Blocks, Header, Identifiers, Regenerate, camel, pascal};
17use yog_codegen_java::Java;
18use yog_data::Shape;
19use yog_data::schema::StructShape;
20
21use types::{TYPES, from_data, java_type, nullable, same, to_data};
22
23/// What the generated file says it is.
24pub(super) const HEADER: Header = Header {
25    tool: "yog-event",
26    source: "Yog-Mod-Loader/rust/crates/yog-event",
27    regenerate: Regenerate::Command("cargo run -p yog-event --bin yog-event-export"),
28};
29
30/// The imports, each added when the file uses it. Java's own types are
31/// written in full (`java.util.List`): a message may be named `List`.
32const IMPORTS: &[(&str, &str)] = &[
33    ("YogBus.", "dev.yog.YogBus"),
34    ("YogData.", "dev.yog.data.YogData"),
35    ("YogFrame.", "dev.yog.data.YogFrame"),
36    ("YogMessage", "dev.yog.YogMessage"),
37];
38
39/// One level of indentation.
40const INDENT: &str = Java::INDENT;
41
42/// A payload field's Java name: `player_name` → `playerName`, `interface` → `interface_`.
43fn field(snake: &str) -> String {
44    Java::ident(&camel(snake))
45}
46
47/// `key_binding` → `KeyBinding`.
48pub(super) fn class_name(segment: &str) -> String {
49    pascal(segment)
50}
51
52/// The file being written, and the payload records it still owes.
53#[derive(Default)]
54struct Out {
55    text: String,
56    records: Vec<StructShape>,
57    /// The class's own members, written before its nested ones.
58    members: String,
59}
60
61/// A record with `s`'s fields: `toData()` builds the payload, `fromData` /
62/// `fromBytes` read one back; `body` adds the message's methods.
63fn record(o: &mut Out, pad: &str, name: &str, s: &StructShape, body: &str) {
64    let Out {
65        text: out, records, ..
66    } = o;
67    let params: Vec<String> = s
68        .fields
69        .iter()
70        .map(|f| format!("{} {}", java_type(&(f.shape)(), records), field(f.name)))
71        .collect();
72    if !s.doc.is_empty() {
73        let _ = writeln!(out, "\n{pad}/** {} */", s.doc);
74    }
75    let _ = writeln!(out, "{pad}public record {name}({}) {{", params.join(", "));
76    let _ = writeln!(
77        out,
78        "{pad}    public java.util.Map<String, Object> toData() {{\n{pad}        java.util.Map<String, Object> m = new java.util.LinkedHashMap<>();"
79    );
80    for f in &s.fields {
81        let shape = (f.shape)();
82        let field = field(f.name);
83        let line = match nullable(&shape) {
84            Some(inner) => format!(
85                "if ({field} != null) m.put(\"{}\", {});",
86                f.name,
87                to_data(&inner, &field, 0)
88            ),
89            None => format!("m.put(\"{}\", {});", f.name, to_data(&shape, &field, 0)),
90        };
91        let _ = writeln!(out, "{pad}        {line}");
92    }
93    let _ = writeln!(out, "{pad}        return m;\n{pad}    }}\n");
94    let args: Vec<String> = s
95        .fields
96        .iter()
97        .map(|f| from_data(&(f.shape)(), &format!("m.get(\"{}\")", f.name), 0))
98        .collect();
99    let _ = writeln!(
100        out,
101        "{pad}    public static {name} fromData(Object data) {{\n{pad}        java.util.Map<String, Object> m = YogData.map(data);\n{pad}        return new {name}({});\n{pad}    }}\n",
102        args.join(", ")
103    );
104    let _ = write!(
105        out,
106        "{pad}    public static {name} fromBytes(byte[] bytes) {{\n{pad}        return fromData(YogData.decode(bytes));\n{pad}    }}"
107    );
108    out.push_str(&frames::codec(name, s, pad));
109    out.push_str(&views::view(name, s, pad));
110    let body = body.replace('\n', &format!("\n{pad}"));
111    let _ = writeln!(out, "{body}\n{pad}}}");
112}
113
114/// `address` in Java: its name in the hosts' `YogNames` (one source), or the
115/// string itself if no name has it.
116fn name(address: &str) -> String {
117    yog_ids::export::java_ref(yog_ids::export::JAVA_CLASS, address)
118        .unwrap_or_else(|| format!("{address:?}"))
119}
120
121/// Where a message goes: its own address (`Address.<constant>`, resolved
122/// once), or its branch for the key field's value.
123fn target(address: &str, key: Option<&str>, constant: &str) -> String {
124    match key {
125        Some(k) => format!("YogBus.keyed({}, {})", name(address), field(k)),
126        None => format!("Address.{constant}"),
127    }
128}
129
130/// Where a listener or provider of a message at `address` is: a keyed
131/// one's branch for the `key` parameter it's given.
132fn listen_target(address: &str, key: Option<&str>, constant: &str) -> String {
133    match key {
134        Some(_) => format!("YogBus.keyed({}, key)", name(address)),
135        None => format!("Address.{constant}"),
136    }
137}
138
139/// The `key` parameter a keyed message's listener or provider takes first.
140fn key_param(key: Option<&str>) -> &'static str {
141    if key.is_some() { "String key, " } else { "" }
142}
143
144/// A message to write: its place in the tree, its name, payload and methods.
145struct Entry {
146    path: &'static str,
147    name: &'static str,
148    shape: Shape,
149    body: String,
150}
151
152/// Closes the classes of `open` down to `keep`.
153fn close(out: &mut String, open: &mut Vec<String>, keep: usize) {
154    while open.len() > keep {
155        open.pop();
156        let _ = writeln!(out, "{}}}", INDENT.repeat(open.len() + 1));
157    }
158}
159
160/// Class `class` (documented `doc`) in `package`: `entries` nested by their paths.
161fn file(
162    package: &str,
163    class: &str,
164    doc: &str,
165    mut o: Out,
166    entries: &[Entry],
167) -> (String, Vec<StructShape>) {
168    let _ = writeln!(
169        o.text,
170        "\n/** {doc} */\npublic final class {class} {{\n    private {class}() {{}}"
171    );
172    let members = std::mem::take(&mut o.members);
173    o.text.push_str(&members);
174    // The class path open now (`Player`, `Session`), closed and opened as the paths go.
175    let mut open: Vec<String> = Vec::new();
176    let mut opened: Vec<String> = Vec::new();
177    for e in entries {
178        let path: Vec<&str> = e.path.split('/').collect();
179        let common = open.iter().zip(&path).take_while(|(a, b)| a == *b).count();
180        close(&mut o.text, &mut open, common);
181        for (depth, seg) in path.iter().enumerate().skip(common) {
182            // A class opened again would be a second class of one name.
183            let whole = path[..=depth].join("/");
184            assert!(
185                !opened.contains(&whole),
186                "the entries of `{whole}` aren't together"
187            );
188            opened.push(whole);
189            let pad = INDENT.repeat(open.len() + 1);
190            let class = class_name(seg);
191            let _ = writeln!(
192                o.text,
193                "\n{pad}/** `{seg}`. */\n{pad}public static final class {class} {{\n{pad}    private {class}() {{}}"
194            );
195            open.push((*seg).to_owned());
196        }
197        let Shape::Struct(s) = &e.shape else { continue };
198        // Java refuses a member type named like a class around it.
199        assert!(
200            open.iter().all(|seg| class_name(seg) != e.name),
201            "{} is named like the class around it ({}): rename it",
202            e.name,
203            e.path
204        );
205        record(&mut o, &INDENT.repeat(open.len() + 1), e.name, s, &e.body);
206    }
207    close(&mut o.text, &mut open, 0);
208    finish(package, o)
209}
210
211/// `class` holding `records` — and the records they hold in turn.
212fn types_file(package: &str, records: Vec<StructShape>) -> String {
213    let mut o = Out {
214        records,
215        ..Out::default()
216    };
217    let _ = writeln!(
218        o.text,
219        "\n/** The records the bus's messages share. */\npublic final class {TYPES} {{\n    private {TYPES}() {{}}"
220    );
221    let mut done = 0;
222    while done < o.records.len() {
223        let s = o.records[done].clone();
224        record(&mut o, INDENT, s.name, &s, "");
225        done += 1;
226    }
227    unique(&o.records);
228    o.text.push_str(&words::classes(&o.records, INDENT));
229    finish(package, o).0
230}
231
232/// The class closed, with its header and imports: its text, and the records it needs.
233fn finish(package: &str, mut o: Out) -> (String, Vec<StructShape>) {
234    o.text.push_str("}\n");
235    let out = o.text;
236    let names = format!(
237        "{}.{}",
238        yog_ids::export::JAVA_PACKAGE,
239        yog_ids::export::JAVA_CLASS
240    );
241    let mut imports: Vec<String> = IMPORTS
242        .iter()
243        .filter(|(used, _)| out.contains(used))
244        .map(|(_, class)| (*class).to_owned())
245        .collect();
246    imports.push(names);
247    imports.sort();
248    let imports: String = imports
249        .iter()
250        .map(|class| format!("import {class};\n"))
251        .collect();
252    let text = format!(
253        "{}package {package};\n\n{imports}{out}",
254        HEADER.comment::<Java>()
255    );
256    (
257        text.lines()
258            .map(str::trim_end)
259            .collect::<Vec<_>>()
260            .join("\n")
261            + "\n",
262        o.records,
263    )
264}
265
266/// Panics when two different payload structs share a record name — they'd
267/// be one class in `YogTypes`.
268fn unique(records: &[StructShape]) {
269    for (i, a) in records.iter().enumerate() {
270        for b in &records[i + 1..] {
271            assert!(
272                a.name != b.name || same(a, b),
273                "two payload structs are named {}: rename one",
274                a.name
275            );
276        }
277    }
278}
279
280/// Payloads that travel inside other bytes (entity data), not as messages:
281/// Java builds them too.
282const PAYLOADS: &[fn() -> Shape] = &[
283    <crate::entity::Hanging as yog_data::Schema>::shape,
284    // The content the runtime hands the hosts at registration (`content_blob`).
285    <crate::content::Content as yog_data::Schema>::shape,
286    // A mirror provider's turn (sent to its mirror's own address).
287    <crate::mirror::Write as yog_data::Schema>::shape,
288    // The host's block mirror's scope (inside a declaration's scope bytes).
289    <crate::mirror::BlockScope as yog_data::Schema>::shape,
290    // The host's mapping table (in its jar, read once).
291    <crate::nanite::Mappings as yog_data::Schema>::shape,
292    // Mods' nanites, read by the host before the game's classes load.
293    <crate::nanite::Nanites as yog_data::Schema>::shape,
294];
295
296/// Every generated class: its name and text.
297pub fn files(package: &str) -> [(&'static str, String); 4] {
298    let (events, mut records) = events::events(package);
299    let (services, more) = services::services(package);
300    for r in more {
301        if !records.iter().any(|have| same(have, &r)) {
302            records.push(r);
303        }
304    }
305    for payload in PAYLOADS {
306        let Shape::Struct(r) = payload() else {
307            continue;
308        };
309        if !records.iter().any(|have| same(have, &r)) {
310            records.push(r);
311        }
312    }
313    unique(&records);
314    [
315        (events::CLASS, events),
316        (services::CLASS, services),
317        (TYPES, types_file(package, records)),
318        (mirrors::CLASS, mirrors::file(package)),
319    ]
320}
321
322#[cfg(test)]
323mod tests {
324    use yog_data::{Schema, Shape};
325
326    use super::unique;
327
328    mod a {
329        #[derive(yog_data::Schema)]
330        pub struct Declare {
331            pub name: String,
332        }
333    }
334
335    mod b {
336        #[derive(yog_data::Schema)]
337        pub struct Declare {
338            pub label: String,
339        }
340    }
341
342    fn shape<T: Schema>() -> yog_data::schema::StructShape {
343        let Shape::Struct(s) = T::shape() else {
344            unreachable!("a derived struct")
345        };
346        s
347    }
348
349    #[test]
350    #[should_panic(expected = "two payload structs are named Declare")]
351    fn two_structs_of_one_name_are_refused() {
352        unique(&[shape::<a::Declare>(), shape::<b::Declare>()]);
353    }
354
355    #[test]
356    fn one_struct_seen_twice_is_fine() {
357        unique(&[shape::<a::Declare>(), shape::<a::Declare>()]);
358    }
359}