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}