Skip to main content

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