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