hexroll3_scroll/
frame.rs

1/*
2// Copyright (C) 2020-2025 Pen, Dice & Paper
3//
4// This program is dual-licensed under the following terms:
5//
6// Option 1: (Non-Commercial) GNU Affero General Public License (AGPL)
7// This program is free software: you can redistribute it and/or modify
8// it under the terms of the GNU Affero General Public License as
9// published by the Free Software Foundation, either version 3 of the
10// License, or (at your option) any later version.
11//
12// This program is distributed in the hope that it will be useful,
13// but WITHOUT ANY WARRANTY; without even the implied warranty of
14// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15// GNU Affero General Public License for more details.
16//
17// You should have received a copy of the GNU Affero General Public License
18// along with this program. If not, see <http://www.gnu.org/licenses/>.
19//
20// Option 2: Commercial License
21// For commercial use, you are required to obtain a separate commercial
22// license. Please contact ithai at pendicepaper.com
23// for more information about commercial licensing terms.
24*/
25use crate::instance::*;
26use crate::repository::*;
27use crate::semantics::Class;
28use anyhow::{Ok, Result};
29
30/// Creates a new entity frame and subscribes it to the classes it collects.
31///
32/// # Arguments
33///
34/// * `tx` - A mutable read/write transaction to load and save frames.
35/// * `parent_uid` - The uid of the parent entity.
36/// * `uid` - The uid of the entity for which to create a frame.
37/// * `class` - The class name of the entity for which to create a frame.
38///
39/// # Returns
40///
41/// * `Result<()>` - Indicates success or failure of the frame creation process.
42pub fn create_entity_frame(
43    tx: &mut ReadWriteTransaction,
44    parent_uid: &str,
45    uid: &str,
46    class: &Class,
47) -> Result<()> {
48    let mut frame = Frame::init(tx, uid, parent_uid)?;
49    for spec in class.collects.iter() {
50        subscribe(&mut frame, &spec.class_name);
51    }
52    let frame_uid = frame.uid;
53    tx.save(&frame_uid)
54}
55
56/// Removes an entity frame from the repository transaction, effectively undoing the
57/// operation performed by `create_entity_frame`.
58///
59/// # Arguments
60///
61/// * `tx` - A mutable read/write transaction to remove the frame.
62/// * `uid` - The uid of the entity frame to be removed.
63///
64/// # Returns
65///
66/// A `Result` indicating success or failure of the removal operation.
67pub fn remove_entity_frame(tx: &mut ReadWriteTransaction, uid: &str) -> Result<()> {
68    tx.remove(&format!("{}_frame", uid))
69}
70
71/// Collect an entity into any subscriber in its frames hierarchy.
72///
73/// This function traverses the frames hierarchy of an entity to collect it
74/// into a subscriber that matches the class name of the entity. The function
75/// continues this process up the hierarchy until it reaches the root frame.
76///
77/// Used when rolling an entity.
78///
79/// # Arguments
80///
81/// * `instance` - A reference to the `SandboxBuilder` instance, providing
82///   necessary context for the sandbox environment.
83/// * `tx` - A mutable read/write transaction to load and save frames.
84/// * `origin_owner_uid` - Uid of the parent of the entity being collected.
85/// * `class_name` - The class name of the entity to be collected.
86/// * `uid` - Uid of the entity being collection.
87///
88/// In the following example, any Wolf rolled from within a Forest
89/// will be collected in the Forest entity frame:
90/// ```text
91/// Wolf {
92///
93/// }
94///
95/// Forest {
96///     << Wolf
97///     wolves @ Wolf
98/// }
99/// ```
100pub fn collect(
101    instance: &SandboxBuilder,
102    tx: &mut ReadWriteTransaction,
103    parent_uid: &str,
104    uid: &str,
105    class_name: &str,
106) -> anyhow::Result<()> {
107    let mut frame_owner_uid: String = parent_uid.to_string();
108    while frame_owner_uid != "root" {
109        let parent_owner_uid = {
110            let frame = tx
111                .load(&format!("{}_frame", frame_owner_uid))
112                .unwrap()
113                .as_frame();
114            for parent in instance.sandbox.classes[class_name].hierarchy.iter() {
115                let unused = &mut frame.obj["$collections"]["$unused"];
116                if unused.as_object().unwrap().contains_key(parent) {
117                    unused[parent]
118                        .as_array_mut()
119                        .unwrap()
120                        .push(serde_json::to_value(uid).unwrap());
121                    break;
122                }
123            }
124            frame.obj["$parent"].clone()
125        };
126        tx.save(&format!("{}_frame", frame_owner_uid))?;
127        frame_owner_uid = parent_owner_uid.as_str().unwrap().to_string();
128    }
129    Ok(())
130}
131
132/// Remove an entity from any collections in its frames hierarchy.
133///
134/// This function traverses the frames hierarchy of an entity to remove
135/// it from any collections that match the class name hierarchy of the entity.
136/// The function continues this process up the hierarchy until it reaches the
137/// root frame. This is the opposite operation of `collect`, providing a means
138/// to 'withdraw' or remove an entity from its frame context.
139///
140/// Used when unrolling an entity.
141///
142/// # Arguments
143///
144/// * `instance` - A reference to the `SandboxBuilder` instance, providing
145///   necessary overview of the sandbox environment.
146/// * `tx` - A mutable read/write transaction to load and save frames.
147/// * `origin_owner_uid` - Uid of the parent of the entity being withdrawn,
148///   previously specified during collection.
149/// * `class_name` - The class name of the entity to be removed from
150///   collections.
151pub fn withdraw(
152    instance: &SandboxBuilder,
153    tx: &mut ReadWriteTransaction,
154    origin_owner_uid: &str,
155    class_name: &str,
156) -> anyhow::Result<()> {
157    let mut frame_owner_uid: String = origin_owner_uid.to_string();
158    while frame_owner_uid != "root" {
159        let parent_owner_uid = {
160            let frame = tx
161                .load(&format!("{}_frame", frame_owner_uid))
162                .unwrap()
163                .as_frame();
164            for parent in instance.sandbox.classes[class_name].hierarchy.iter() {
165                let unused = &mut frame.obj["$collections"]["$unused"];
166                if unused.as_object().unwrap().contains_key(parent) {
167                    unused[parent]
168                        .as_array_mut()
169                        .unwrap()
170                        .retain(|v| v != origin_owner_uid);
171                }
172            }
173            for parent in instance.sandbox.classes[class_name].hierarchy.iter() {
174                let used = &mut frame.obj["$collections"]["$unused"];
175                if used.as_object().unwrap().contains_key(parent) {
176                    used[parent]
177                        .as_array_mut()
178                        .unwrap()
179                        .retain(|v| v != origin_owner_uid);
180                }
181            }
182            frame.obj["$parent"].clone()
183        };
184        tx.save(&format!("{}_frame", frame_owner_uid))?;
185        frame_owner_uid = parent_owner_uid.as_str().unwrap().to_string();
186    }
187    Ok(())
188}
189
190/// Attempts to select a random unused entity of the specified class from the frame hierarchy
191/// associated with the given owner. If available, the selected entity is marked as used and
192/// returned. The search traverses up the hierarchy until an entity is found or the root is reached.
193/// If no entity is found, `None` is returned.
194/// The selected entity will not be available for other use requests
195/// until it will get recycled (through a `recycle` call).
196///
197/// # Arguments
198///
199/// * `instance` - Provides access to the sandbox environment, including randomization.
200/// * `tx` - A mutable read/write transaction to load and save frames.
201/// * `origin_owner_uid` - The UID of the initial frame owner where the search begins.
202/// * `class_name` - The class name of the entity to be selected.
203///
204/// # Returns
205///
206/// `Ok(Some(String))` containing the UID of the selected entity if one is found,
207/// `Ok(None)` if no entity is available, or an error if the transaction fails.
208pub fn use_collected(
209    instance: &SandboxBuilder,
210    tx: &mut ReadWriteTransaction,
211    origin_owner_uid: &str,
212    class_name: &str,
213) -> Result<Option<String>> {
214    let mut ret: Option<String> = None;
215
216    let mut frame_owner_uid: String = origin_owner_uid.to_string();
217    while frame_owner_uid != "root" && ret.is_none() {
218        let parent_owner_uid = {
219            let frame = tx
220                .load(&format!("{}_frame", frame_owner_uid))
221                .unwrap()
222                .as_frame();
223            let unused = &mut frame.obj["$collections"]["$unused"];
224            if unused.as_object().unwrap().contains_key(class_name) {
225                let unused_list = unused[class_name].as_array().unwrap();
226                if unused_list.is_empty() {
227                    return Ok(None);
228                }
229                let selected = instance
230                    .randomizer
231                    .in_range(0, unused_list.len() as i32 - 1);
232
233                let selected_uid = unused_list[selected as usize].clone();
234                unused[class_name]
235                    .as_array_mut()
236                    .unwrap()
237                    .remove(selected as usize);
238                frame.obj["$collections"]["$used"][class_name]
239                    .as_array_mut()
240                    .unwrap()
241                    .push(selected_uid.clone());
242                ret = Some(selected_uid.as_str().unwrap().to_string());
243            }
244            frame.obj["$parent"].clone()
245        };
246        tx.save(&format!("{}_frame", frame_owner_uid))?;
247        frame_owner_uid = parent_owner_uid.as_str().unwrap().to_string();
248    }
249    Ok(ret)
250}
251
252/// Recycle a used entity and make it available again.
253/// This is the inverse operation to `use_collected`.
254///
255/// # Arguments
256///
257/// * `tx` - A mutable read/write transaction to load and save frames.
258/// * `origin_owner_uid` - The UID of the initial frame owner starting the recycle process.
259/// * `uid_to_recycle` - The UID of the entity to be recycled.
260/// * `class_name` - The class name of the entity to be recycled.
261///
262/// # Returns
263///
264/// A `Result` indicating success or failure of the operation.
265pub fn recycle(
266    tx: &mut ReadWriteTransaction,
267    origin_owner_uid: &str,
268    uid_to_recycle: &str,
269    class_name: &str,
270) -> Result<()> {
271    let mut frame_owner_uid: String = origin_owner_uid.to_string();
272    while frame_owner_uid != "root" {
273        let parent_owner_uid = {
274            let frame = tx
275                .load(&format!("{}_frame", frame_owner_uid))
276                .unwrap()
277                .as_frame();
278            let unused = &mut frame.obj["$collections"]["$unused"];
279            if unused.as_object().unwrap().contains_key(class_name) {
280                unused[class_name]
281                    .as_array_mut()
282                    .unwrap()
283                    .push(serde_json::Value::from(uid_to_recycle));
284                frame.obj["$collections"]["$used"][class_name]
285                    .as_array_mut()
286                    .unwrap()
287                    .retain(|uid| uid != uid_to_recycle);
288            }
289            frame.obj["$parent"].clone()
290        };
291        tx.save(&format!("{}_frame", frame_owner_uid))?;
292        frame_owner_uid = parent_owner_uid.as_str().unwrap().to_string();
293    }
294    Ok(())
295}
296
297/// Attempts to select a random entity of the specified class from the frame hierarchy
298/// associated with the given owner.
299/// Picking a collected entity is different from **using** a collected entity in that
300/// the picked entity can be selected again by other callers.
301///
302/// # Arguments
303///
304/// * `instance` - Provides access to the sandbox environment, including randomization.
305/// * `tx` - A mutable read/write transaction to load and save frames.
306/// * `origin_owner_uid` - The UID of the initial frame owner where the search begins.
307/// * `class_name` - The class name of the entity to be selected.
308///
309/// # Returns
310///
311/// `Ok(Some(String))` containing the UID of the selected entity if one is found,
312/// `Ok(None)` if no entity is available, or an error if the transaction fails.
313pub fn pick_collected(
314    instance: &SandboxBuilder,
315    tx: &mut ReadWriteTransaction,
316    origin_owner_uid: &str,
317    class_name: &str,
318) -> Result<Option<String>> {
319    let mut ret: Option<String> = None;
320
321    let mut frame_owner_uid: String = origin_owner_uid.to_string();
322    while frame_owner_uid != "root" {
323        let parent_owner_uid = {
324            let frame = tx
325                .load(&format!("{}_frame", frame_owner_uid))
326                .unwrap()
327                .as_frame();
328            let unused = &mut frame.obj["$collections"]["$unused"];
329            if unused.as_object().unwrap().contains_key(class_name) {
330                let unused_list = unused[class_name].as_array().unwrap();
331                if unused_list.is_empty() {
332                    return Ok(None);
333                }
334                let selected = instance
335                    .randomizer
336                    .in_range(0, unused_list.len() as i32 - 1);
337
338                let selected_uid = unused_list[selected as usize].clone();
339                ret = Some(selected_uid.as_str().unwrap().to_string());
340                break;
341            }
342            frame.obj["$parent"].clone()
343        };
344        tx.save(&format!("{}_frame", frame_owner_uid))?;
345        frame_owner_uid = parent_owner_uid.as_str().unwrap().to_string();
346    }
347    Ok(ret)
348}
349
350/// Every entity has a Frame record stored in the format of:
351/// {}_frame.
352///
353/// The entity frame is designed to store data only required
354/// during modifications (rolling, unrolling, appending etc.)
355///
356/// This is done for efficiency consdirations.
357/// Frame data is almost never used during rendering, with
358/// some very rare exceptions.
359pub struct Frame<'a> {
360    pub uid: String,
361    pub obj: &'a mut serde_json::Value,
362}
363
364impl<'a> Frame<'a> {
365    pub fn from_value(v: &'a mut serde_json::Value) -> Self {
366        Frame {
367            uid: v["uid"].as_str().unwrap().to_string(),
368            obj: v,
369        }
370    }
371
372    pub fn init(
373        tx: &'a mut ReadWriteTransaction,
374        uid2: &'a str,
375        parent_uid: &'a str,
376    ) -> Result<Self> {
377        let frame_uid = format!("{}_frame", uid2);
378        let frame = tx.create(&frame_uid)?.as_frame();
379        frame.obj["$parent"] = serde_json::Value::from(parent_uid);
380        let collections = &mut frame.obj["$collections"];
381        collections["$unused"] = serde_json::json!({});
382        collections["$used"] = serde_json::json!({});
383        Ok(frame)
384    }
385}
386
387pub trait FrameConvertor<'a> {
388    fn as_frame(&'a mut self) -> Frame<'a>;
389}
390
391impl<'b> FrameConvertor<'b> for serde_json::Value {
392    fn as_frame(&'b mut self) -> Frame<'b> {
393        Frame::from_value(self)
394    }
395}
396
397/// Subscribe the frame to a class
398fn subscribe(frame: &mut Frame, class_name: &str) {
399    let collections = &mut frame.obj["$collections"];
400    collections["$unused"][class_name] = serde_json::json!([]);
401    collections["$used"][class_name] = serde_json::json!([]);
402}