1 /**
2 	SaveScenario.c
3  	Scenario saving functionality
4 
5 	@author Sven2
6 */
7 
8 // Defines script function SaveScenarioObjects, which is called by the
9 // engine to generate the Objects.c file for scenario saving
10 // Also called for object duplication in the editor
11 
12 // Temp variables used by MakeScenarioSaveName()
13 //   These variables could be passed as a parameter through all saving functions instead.
14 //   But that would include every single SaveScenarioObject call and associated functions and would be easy for scripters to forget.
15 static save_scenario_obj_dependencies; // Dependency graph to ensure objects are saved in proper order
16 static save_scenario_def_indices;      // Used to generate unique indices in variable names
17 static save_scenario_dup_objects;      // Objects to duplicate if SaveScenarioObjects is called for object duplication.
18 
19 // Propert identifier of object creation
20 static const SAVEOBJ_Creation = "Creation";
21 static const SAVEOBJ_ContentsCreation = "ContentsCreation";
22 static const SAVEOBJ_ContentsCreationEx = "ContentsCreationEx";
23 
SaveScenarioObjects(f,duplicate_objects)24 global func SaveScenarioObjects(f, duplicate_objects)
25 {
26 	// f is a handle to the Objects.c file
27 	// If called for object duplication, duplicate_objects is an array of objects to duplicate
28 	save_scenario_dup_objects = duplicate_objects;
29 	// Prepare props saving object
30 	var props_prototype = {
31 		Add = Global.SaveScenP_Add,
32 		AddSet = Global.SaveScenP_AddSet,
33 		AddCall = Global.SaveScenP_AddCall,
34 		Remove = Global.SaveScenP_Remove,
35 		RemoveCreation = Global.SaveScenP_RemoveCreation,
36 		Clear = Global.SaveScenP_Clear,
37 		Buffer2File = Global.SaveScenP_Buffer2File,
38 		HasData = Global.SaveScenP_HasData,
39 		HasCreation = Global.SaveScenP_HasCreation,
40 		HasProps = Global.SaveScenP_HasProps,
41 		HasProp = Global.SaveScenP_HasProp,
42 		TakeProps = Global.SaveScenP_TakeProps
43 	};
44 	// Write all (scenario) or specified (duplication) objects!
45 	var objs = duplicate_objects, obj, i;
46 	if (!objs) objs = FindObjects(Find_And());
47 	var n = GetLength(objs);
48 	var obj_type, any_written, do_write_file = false;
49 	save_scenario_def_indices = nil;
50 	// In reverse order (background to foreground)
51 	for (i=0; i<n/2; ++i) { obj = objs[i]; objs[i] = objs[n-i-1]; objs[n-i-1] = obj; }
52 	// ...Except player crew
53 	var ignore_objs = [];
54 	if (!save_scenario_dup_objects)
55 	{
56 		for (var iplr = 0; iplr < GetPlayerCount(C4PT_User); ++iplr)
57 		{
58 			for (var icrew = 0, crew; crew = GetCrew(GetPlayerByIndex(iplr, C4PT_User), icrew); ++icrew)
59 			{
60 				ignore_objs[GetLength(ignore_objs)] = crew;
61 			}
62 		}
63 	}
64 	// Ignore objects tagged with a no-save effect
65 	for (obj in objs)
66 	{
67 		if (GetEffect("IntNoScenarioSave", obj))
68 		{
69 			ignore_objs[GetLength(ignore_objs)] = obj;
70 		}
71 	}
72 	// Write creation data and properties
73 	var obj_data = SaveScen_Objects(objs, ignore_objs, props_prototype);
74 	// Resolve dependencies
75 	obj_data = SaveScen_ResolveDepends(objs, obj_data);
76 	// Write scripts to force containers
77 	obj_data = SaveScen_SetContainers(obj_data);
78 	// Write header
79 	FileWrite(f, "/* Automatically created objects file */\n\n");
80 	// Declare static variables for objects that wish to have them
81 	for (obj in objs)
82 	{
83 		if (obj.StaticSaveVar && !save_scenario_dup_objects)
84 		{
85 			if (!any_written) FileWrite(f, "static "); else FileWrite(f, ", ");
86 			FileWrite(f, obj.StaticSaveVar);
87 			any_written = true;
88 		}
89 	}
90 	if (any_written)
91 	{
92 		FileWrite(f, ";\n\n");
93 		any_written = false;
94 	}
95 	// Write all the creation data
96 	FileWrite(f, "func InitializeObjects()\n{\n");
97 	any_written = false;
98 	for (obj in obj_data)
99 	{
100 		var spacing = "	";
101 		if (obj_type != obj.o->GetID())
102 		{
103 			if (any_written) spacing = "\n	"; // Extra spacing between different object types
104 			obj_type = obj.o->GetID();
105 			any_written = false;
106 		}
107 		if (obj.o.StaticSaveVar && !save_scenario_dup_objects)
108 		{
109 			if (obj.props->HasCreation()) FileWrite(f, Format("%s%s = ", spacing, obj.o.StaticSaveVar));
110 		}
111 		else if (obj.write_label)
112 		{
113 			FileWrite(f, Format("%svar %s", spacing, obj.o->MakeScenarioSaveName()));
114 			if (obj.props->HasCreation()) FileWrite(f, " = "); else FileWrite(f, ";\n");
115 		}
116 		else if (obj.props->HasCreation())
117 		{
118 			FileWrite(f, spacing);
119 		}
120 		if (obj.props->~Buffer2File(f)) do_write_file = any_written = true;
121 	}
122 	// Write global effects
123 	if (!save_scenario_dup_objects)
124 	{
125 		any_written = false;
126 		var fx; i=0;
127 		while (fx = GetEffect("*", nil, i++))
128 		{
129 			var fx_buffer = {Prototype=props_prototype};
130 			EffectCall(nil, fx, "SaveScen", fx_buffer);
131 			if (fx_buffer->HasData())
132 			{
133 				if (!any_written && do_write_file) FileWrite(f, "	\n");
134 				any_written = do_write_file = true;
135 				fx_buffer->~Buffer2File(f);
136 			}
137 		}
138 	}
139 	// Cleanup
140 	save_scenario_def_indices = save_scenario_obj_dependencies = save_scenario_dup_objects = nil;
141 	// Write footer
142 	FileWrite(f, "	return true;\n}\n");
143 	// Done; success. Return true if any objects or effects were written to the file.
144 	return do_write_file;
145 }
146 
SaveScen_Objects(array objs,array ignore_objs,proplist props_prototype)147 global func SaveScen_Objects(array objs, array ignore_objs, proplist props_prototype)
148 {
149 	// Write all object data into buffers
150 	var n = GetLength(objs);
151 	var obj_data = CreateArray(n), obj;
152 	for (var i=0; i<n; ++i)
153 	{
154 		obj = objs[i];
155 		obj_data[i] = { o=obj, props={Prototype=props_prototype} };
156 		obj._save_scen_objdata = obj_data[i];
157 	}
158 	for (var i=0; i<n; ++i)
159 	{
160 		obj = objs[i];
161 		// Skip objects on ignore list (check for all objects up the containment chain)
162 		var is_ignored = false;
163 		var container_obj = obj;
164 		while (container_obj)
165 		{
166 			if (GetIndexOf(ignore_objs, container_obj) >= 0) is_ignored = true;
167 			if (WildcardMatch(Format("%i", container_obj->GetID()), "GUI_*")) is_ignored = true; // big bunch of objects that should not be stored.
168 			container_obj = container_obj->Contained();
169 		}
170 		if (is_ignored) continue;
171 		// Generate object creation and property strings
172 		save_scenario_obj_dependencies = [];
173 		if (!obj->SaveScenarioObject(obj_data[i].props))
174 		{
175 			obj_data[i].props->Clear();
176 			continue;
177 		}
178 		if (obj->Contained()) obj->Contained()->MakeScenarioSaveName(); // force container dependency
179 		if (obj_data[i].props->HasProps()) obj_data[i].write_label = true;
180 		obj_data[i].dependencies = save_scenario_obj_dependencies;
181 		obj_data[i].n_dependencies = GetLength(save_scenario_obj_dependencies);
182 		save_scenario_obj_dependencies = nil;
183 	}
184 	return obj_data;
185 }
186 
SaveScen_ResolveDepends(array objs,array obj_data)187 global func SaveScen_ResolveDepends(array objs, array obj_data)
188 {
189 	// Dependency pointer from obj to obj_data
190 	var i,j,k,n=GetLength(objs),od;
191 	for (i=0; i<n; ++i)
192 	{
193 		for (j=0; j<obj_data[i].n_dependencies; ++j)
194 		{
195 			k = GetIndexOf(objs, obj_data[i].dependencies[j]);
196 			if (k < 0 || obj_data[i].dependencies[j] == obj_data[i].o) obj_data[i].dependencies[j] = nil; else obj_data[i].dependencies[j] = obj_data[k];
197 		}
198 		if (objs[i]->Contained())
199 		{
200 			k = GetIndexOf(objs, objs[i]->Contained());
201 			if (k >= 0) obj_data[i].co = obj_data[k];
202 		}
203 	}
204 	// Resolve dependencies
205 	k = 0;
206 	for (i=0; i<n; ++i)
207 	{
208 		k = Max(k, i);
209 		od = obj_data[i];
210 		while (od.i_dep_resolved < od.n_dependencies)
211 		{
212 			var dep = od.dependencies[od.i_dep_resolved++];
213 			if (dep)
214 			{
215 				j = GetIndexOf(obj_data, dep);
216 				if (j > i)
217 				{
218 					// The dependent object is behind us in the list. This is bad!
219 					if (!od.props->HasCreation())
220 					{
221 						// This is just object properties. Move them behind dependent object creation
222 						var obj_data_new = CreateArray(n);
223 						obj_data_new[0:i] = obj_data[0:i];
224 						obj_data_new[i:j] = obj_data[i+1:j+1];
225 						obj_data_new[j] = obj_data[i];
226 						if (j<n-1) obj_data_new[j+1:n] = obj_data[j+1:n];
227 						obj_data = obj_data_new;
228 						if (j <= k) --k;
229 						--i; break;
230 					}
231 					else if (j <= k)
232 					{
233 						// Circular dependency. Detach object property setting from object creation.
234 						var obj_data_new = CreateArray(++n);
235 						obj_data_new[0:j+1] = obj_data[0:j+1];
236 						obj_data_new[j+1] = { o=od.o, co=od.co, dependencies=od.dependencies, n_dependencies=od.n_dependencies, i_dep_resolved=od.i_dep_resolved, props=od.props->TakeProps() };
237 						od.n_dependencies = 0; od.props_detached = true;
238 						if (j<n-1) obj_data_new[j+2:n] = obj_data[j+1:n];
239 						obj_data = obj_data_new;
240 						++k; break;
241 					}
242 					else
243 					{
244 						// No circular dependency. Just move object we depend on in front of us and process its dependencies.
245 						var obj_data_new = CreateArray(n);
246 						if (i) obj_data_new[0:i] = obj_data[0:i];
247 						obj_data_new[i] = obj_data[j];
248 						obj_data_new[i+1:j+1] = obj_data[i:j];
249 						if (j<n-1) obj_data_new[j+1:n] = obj_data[j+1:n];
250 						obj_data = obj_data_new;
251 						++k; --i; break;
252 					}
253 				}
254 			}
255 		}
256 	}
257 	// Free up all dependency data
258 	for (od in obj_data)
259 	{
260 		od.dependencies = nil;
261 		od.o._save_scen_objdata = nil;
262 	}
263 	return obj_data;
264 }
265 
SaveScen_SetContainers(array obj_data)266 global func SaveScen_SetContainers(array obj_data)
267 {
268 	// Ensure that objects are in proper container
269 	// Replace calls to CreateObjectAbove with CreateContents.
270 	for (var obj in obj_data) if (obj.o->Contained())
271 	{
272 		// ignore if container object was not saved
273 		if (obj.co && obj.co.props->HasCreation())
274 		{
275 			// creation and props? Then turn into CreateContents
276 			if (obj.props->HasProp(SAVEOBJ_ContentsCreation) && !obj.props_detached)
277 			{
278 				obj.props->Remove(SAVEOBJ_Creation);
279 				// Adjust owner if necessery
280 				if (obj.o->GetOwner() != obj.o->Contained()->GetOwner())
281 					obj.props->AddCall("Owner", obj.o, "SetOwner", obj.o->GetOwner());
282 			}
283 			else if (obj.props.origin)
284 			{
285 				// props detached from creation. Use Enter() call to enter container
286 				// the label must have been written because something depended on the object.
287 				obj.props.origin->Remove(SAVEOBJ_ContentsCreation);
288 				obj.props.origin->Remove(SAVEOBJ_ContentsCreationEx);
289 				obj.props->AddCall("Container", obj.o, "Enter", obj.o->Contained());
290 				// Ensure layer is written for detached contents if it is the same as the container
291 				var o_layer = obj.o->GetObjectLayer();
292 				if (o_layer && o_layer == obj.co->GetObjectLayer()) obj.props->AddCall("Layer", obj.o, "SetObjectLayer", o_layer);
293 			}
294 		}
295 		else
296 		{
297 			// unsaved container - just create object outside
298 			obj.props->Remove(SAVEOBJ_ContentsCreation);
299 			obj.props->Remove(SAVEOBJ_ContentsCreationEx);
300 		}
301 	}
302 	// Concatenate multiple contents creations
303 	var cont;
304 	for (var obj in obj_data) if ((cont = obj.o->Contained())) if (obj.props->HasProp(SAVEOBJ_ContentsCreationEx))
305 	{
306 		var num_contents_concat = 1;
307 		if ((!obj.o.StaticSaveVar || save_scenario_dup_objects) && !obj.write_label)
308 		{
309 			for (var obj2 in obj_data) if (obj2 != obj && obj2.o->Contained() == cont && obj.o->GetID() == obj2.o->GetID() && obj2.props->HasProp(SAVEOBJ_ContentsCreationEx))
310 			{
311 				++num_contents_concat;
312 				obj2.props->Clear();
313 			}
314 		}
315 		if (num_contents_concat > 1)
316 		{
317 			obj.props->Remove(SAVEOBJ_ContentsCreation);
318 			var creation_prop = obj.props->HasProp(SAVEOBJ_ContentsCreationEx);
319 			creation_prop.s = Format(creation_prop.s, num_contents_concat);
320 		}
321 		else
322 			obj.props->Remove(SAVEOBJ_ContentsCreationEx);
323 	}
324 	return obj_data;
325 }
326 
AddScenarioSaveDependency()327 global func AddScenarioSaveDependency()
328 {
329 	// Remember this object in the list of dependencies for the currently processed object
330 	if (save_scenario_obj_dependencies && GetIndexOf(save_scenario_obj_dependencies, this)<0) save_scenario_obj_dependencies[GetLength(save_scenario_obj_dependencies)] = this;
331 }
332 
333 // documented in /docs/sdk/script/fn
MakeScenarioSaveName()334 global func MakeScenarioSaveName()
335 {
336 	// Get name to be used to store this object in a scenario
337 	if (!this) FatalError("MakeScenarioSaveName needs definition or object context!");
338 	// Definitions may just use their regular name
339 	if (this.Prototype == Global) return Format("%i", this);
340 	// Duplication mode: If this is an object that is not being duplicated, just reference it as Object(number)
341 	if (save_scenario_dup_objects && GetIndexOf(save_scenario_dup_objects, this)<0) return Format("%v", this);
342 	// When the name is queried while properties are built, it means that there is a dependency. Store it.
343 	AddScenarioSaveDependency();
344 	// Write name if it had been used elsewhere
345 	if (this._save_scen_objdata) this._save_scen_objdata.write_label = true;
346 	// Build actual name using unique number (unless there's a static save variable name for us)
347 	if (this.StaticSaveVar && !save_scenario_dup_objects) return this.StaticSaveVar;
348 	if (!save_scenario_def_indices) save_scenario_def_indices = {};
349 	var base_name = Format("%i", GetID());
350 	if (base_name == "") base_name = "Unknown";
351 	// save_scenario_def_indices is a proplist containing arrays of all objects sorted by type
352 	// the saved name is <ID><index>, where index is the 1-based index into the array
353 	var def_indices = save_scenario_def_indices[base_name];
354 	var idx;
355 	if (!def_indices)
356 	{
357 		save_scenario_def_indices[base_name] = def_indices = [this];
358 		idx = 0;
359 	}
360 	else
361 	{
362 		idx = GetIndexOf(def_indices, this);
363 		if (idx<0)
364 		{
365 			idx = GetLength(def_indices);
366 			def_indices[idx] = this;
367 		}
368 	}
369 	return Format("%s%03d", base_name, idx+1);
370 }
371 
SaveScenarioObject(props)372 global func SaveScenarioObject(props)
373 {
374 	// Called in object context: Default object writing procedure
375 	// Overwrite this method and return false for objects that should not be saved
376 	// Overwrite and call inherited for objects that add/remove/alter default creation/properties
377 	var owner_string = "", i;
378 	if (GetOwner() != NO_OWNER) owner_string = Format(", %d", GetOwner());
379 	// Object creation: Default is to create above bottom center point
380 	// This usually works well with stuff like buildings that may change size in updated versions
381 	// The center point is usually what's of interest for rotated objects, contained objects and InEarth material as well as some objects that explicitely state different creation
382 	var is_centered_creation = (!this.SaveScenarioCreateFromBottom) && (GetR() || Contained() || GBackSolid() || (GetCategory() & (C4D_Rule | C4D_Goal | C4D_Environment)) || this.SaveScenarioCreateCentered);
383 	if (is_centered_creation)
384 	{
385 		if (!GetX() && !GetY() && (owner_string == ""))
386 			props->Add(SAVEOBJ_Creation, "CreateObject(%i)", GetID()); // super-short version for e.g. goals/rules at position 0/0
387 		else
388 			props->Add(SAVEOBJ_Creation, "CreateObject(%i, %d, %d%s)", GetID(), GetX(), GetY(), owner_string);
389 	}
390 	else
391 	{
392 		props->Add(SAVEOBJ_Creation, "CreateObjectAbove(%i, %d, %d%s)", GetID(), GetX(), GetDefBottom(), owner_string);
393 	}
394 	// Contained creation is added alongside regular creation because it is not yet known if CreateObject+Enter or CreateContents can be used due to dependencies.
395 	// func SaveScen_SetContainers will take care of removing one of the two creation strings after dependencies have been resolved.
396 	if (Contained())
397 	{
398 		props->Add(SAVEOBJ_ContentsCreation, "%s->CreateContents(%i)", Contained()->MakeScenarioSaveName(), GetID());
399 		props->Add(SAVEOBJ_ContentsCreationEx, "%s->CreateContents(%i, %%d)", Contained()->MakeScenarioSaveName(), GetID());
400 	}
401 	// Write some default props every object should save
402 	var v, is_static = (GetCategory() & C4D_StaticBack) || Contained(), def = GetID();
403 	v = GetAlive();         if (!v && (GetCategory()&C4D_Living)) props->AddCall("Alive",         this, "Kill", this, true);
404 	v = GetDir();           if (v)                                props->AddCall("Dir",           this, "SetDir", GetConstantNameByValueSafe(v,"DIR_"));
405 	v = GetComDir();        if (v)                                props->AddCall("ComDir",        this, "SetComDir", GetConstantNameByValueSafe(v,"COMD_"));
406 	v = GetCon();           if (v != 100)                         props->AddCall("Con",           this, "SetCon", Max(v,1));
407 	v = GetCategory();      if (v != def->GetCategory())          props->AddCall("Category",      this, "SetCategory", GetBitmaskNameByValue(v, "C4D_"));
408 	v = GetR();             if (v && !Contained())                props->AddCall("R",             this, "SetR", v);
409 	v = GetXDir();          if (v && !is_static)                  props->AddCall("XDir",          this, "SetXDir", v);
410 	v = GetYDir();          if (v && !is_static) if (!Inside(v, 1,12) || !GetContact(-1, CNAT_Bottom))
411 	                                                              props->AddCall("YDir",          this, "SetYDir", v); // consolidate small YDir for standing objects
412 	v = GetRDir();          if (v && !is_static)                  props->AddCall("RDir",          this, "SetRDir", v);
413 	var default_color = 0xffffffff;
414 	if (GetDefColorByOwner()) if (GetOwner() == NO_OWNER) default_color = 0xff; else default_color = GetPlayerColor(GetOwner());
415 	v = GetColor();         if (v && v != default_color)          props->AddCall("Color",         this, "SetColor", Format("0x%x", v));
416 	v = GetClrModulation(); if (v && v != 0xffffffff)             props->AddCall("ClrModulation", this, "SetClrModulation", Format("0x%08x", v));
417 	v = GetObjectBlitMode();if (v)                                props->AddCall("BlitMode",      this, "SetObjectBlitMode", GetBitmaskNameByValue(v & ~GFX_BLIT_Custom, "GFX_BLIT_"));
418 	for (i=0; v=def->GetMeshMaterial(i); ++i)
419 	                        if (GetMeshMaterial(i) != v)          props->AddCall("MeshMaterial",  this, "SetMeshMaterial", Format("%v", GetMeshMaterial(i)), i);
420 	v = this.Name;          if (v != def.Name)                    props->AddCall("Name",          this, "SetName", SaveScenarioValue2String(v));
421 	v = this.MaxEnergy;     if (v != def.MaxEnergy)               props->AddSet ("MaxEnergy",     this, "MaxEnergy", this.MaxEnergy);
422 	v = GetEnergy();        if (v != def.MaxEnergy/1000)          props->AddCall("Energy",        this, "DoEnergy", v-def.MaxEnergy/1000);
423 	v = this.Visibility;    if (v != def.Visibility)              props->AddSet ("Visibility",    this, "Visibility", SaveScenarioValue2String(v, "VIS_", true));
424 	v = this.Plane;         if (v != def.Plane)                   props->AddSet ("Plane",         this, "Plane", v);
425 	v = GetObjectLayer(); var def_layer=nil; if (Contained()) def_layer = Contained()->GetObjectLayer();
426 	                        if (v != def_layer)                   props->AddCall("Layer",         this, "SetObjectLayer", v);
427 	v = this.LineColors;    if (v != def.LineColors)              props->AddSet ("LineColors",    this, "LineColors", v);
428 	v = this.StaticSaveVar; if (v && !save_scenario_dup_objects)  props->AddSet ("StaticSaveVar", this, "StaticSaveVar", Format("%v", v)); // do not duplicate StaticSaveVar because it needs to be unique
429 	// Commands: Could store the whole command stack using AppendCommand.
430 	// However, usually there is one base command and the rest is derived
431 	// (e.g.: A Get command may lead to multiple MoveTo commands to the
432 	// target object). So just store the topmost command.
433 	var command, last_command; i=0;
434 	while (command = GetCommand(0, i++)) last_command = command;
435 	if (last_command)
436 	{
437 		i-=2;
438 		props->AddCall("Command", this, "SetCommand", Format("%v", last_command),
439 				SaveScenarioValue2String(GetCommand(1,i)), // target
440 				SaveScenarioValue2String(GetCommand(2,i)), // x
441 				SaveScenarioValue2String(GetCommand(3,i)), // y
442 				SaveScenarioValue2String(GetCommand(4,i)), // target2
443 				SaveScenarioValue2String(GetCommand(5,i))); // data
444 	}
445 	// Effects
446 	var fx; i=0;
447 	while (fx = GetEffect("*", this, i++)) EffectCall(this, fx, "SaveScen", props);
448 	// EditorProps
449 	if (this.EditorProps)
450 	{
451 		var all_prop_names = GetProperties(this.EditorProps), prop_name, prop;
452 		for (prop_name in all_prop_names)
453 		{
454 			if ((prop=this.EditorProps[prop_name]))
455 			{
456 				if (GetType(prop) == C4V_PropList)
457 				{
458 					if (prop.Save)
459 					{
460 						v = this[prop_name];
461 						var default_v = GetID()[prop_name];
462 						if (!DeepEqual(v, default_v))
463 						{
464 							if (prop.Set)
465 							{
466 								// Save as call
467 								props->AddCall(prop.Save, this, prop.Set, SaveScenarioValue2String(v));
468 							}
469 							else
470 							{
471 								// Save as direct value setting
472 								props->AddSet(prop.Save, this, prop_name, SaveScenarioValue2String(v));
473 							}
474 						}
475 					}
476 				}
477 			}
478 		}
479 	}
480 	// A con of != 100 and a non-zero rotation may have moved the object, if so re-center it after setting the con and rotation.
481 	if (is_centered_creation && (GetCon != 100 || (GetR() && !Contained())))
482 		props->AddCall("SetPosition", this, "SetPosition", GetX(), GetY());
483 	// Automatic unsticking for items lying on the ground and for animals / clonks
484 	// Do this after Con/Rotation and other calls that may affect the shape
485 	// (Note: If someone loads a game in paused mode and immediately saves without unpausing, most unstick calls for items will be lost)
486 	if (!Contained() && !this.SaveScenarioCreateCentered && !this.SaveScenarioCreateFromBottom && !Stuck() && (GetAlive() || (this.Collectible && GetContact(-1, CNAT_Left | CNAT_Right | CNAT_Top | CNAT_Bottom))))
487 	{
488 		var unstick_range = 7; // GetScenMapZoom() - 1; // Unfortunately, this would not be sync save for network clients doing runtime join on editor sessions [end then reloading from a runtime save]
489 		props->AddCall("Unstick", this, "Unstick", unstick_range);
490 	}
491 	// Initialization function as late as possible
492 	v = this.CustomInitializationScript; if (v) props->AddCall("CustomInitialization", this, "CustomInitialize", Format("%v", v));
493 	return true;
494 }
495 
496 // documented in /docs/sdk/script/fn
SaveScenarioObjectAction(props)497 global func SaveScenarioObjectAction(props)
498 {
499 	// Helper function to store action properties
500 	var v;
501 	props->AddCall("Action", this, "SetAction", Format("%v", GetAction() ?? "Idle"), GetActionTarget(), GetActionTarget(1));
502 	if (v = GetPhase()) props->AddCall("Phase", this, "SetPhase", v);
503 	return true;
504 }
505 
506 
FxFireSaveScen(object obj,proplist fx,proplist props)507 global func FxFireSaveScen(object obj, proplist fx, proplist props)
508 {
509 	// this is burning. Save incineration to scenario.
510 	props->AddCall("Fire", obj, "Incinerate", fx.strength, fx.caused_by, fx.blasted, fx.incinerating_object);
511 	return true;
512 }
513 
514 
515 /* Helper functions for value formatting */
516 
517 // Helper function to turn values of several types into a strings to be written to Objects.c
SaveScenarioValue2String(v,string constant_prefix,bool allow_bitmask)518 global func SaveScenarioValue2String(v, string constant_prefix, bool allow_bitmask)
519 {
520 	var rval, vt = GetType(v);
521 	if (vt == C4V_C4Object) return v->MakeScenarioSaveName();
522 	if (vt == C4V_Array) // save procedure for arrays: recurse into contents (cannot save arrays pointing into itself that way)
523 	{
524 		for (var el in v)
525 		{
526 			if (rval) rval = Format("%s,%s", rval, SaveScenarioValue2String(el, constant_prefix, allow_bitmask));
527 			else rval = SaveScenarioValue2String(el, constant_prefix, allow_bitmask);
528 			constant_prefix = nil; // Only first element is actual bitmask (at least or VIS_, and that's the only user for this case)
529 		}
530 		if (rval) rval = Format("[%s]", rval); else rval = "[]";
531 		return rval;
532 	}
533 	// custom save procedure for some prop lists or definitions
534 	if (vt == C4V_PropList || vt == C4V_Def)
535 	{
536 		rval = v->~ToString();
537 		if (rval) return rval;
538 		// proplist saving
539 		if (vt == C4V_PropList)
540 		{
541 			var props = GetProperties(v);
542 			for (var el in props)
543 			{
544 				if (GetChar(el) == GetChar("_")) continue; // props starting with underscore are not to be saved
545 				if (rval) rval = Format("%s,%s=%s", rval, el, SaveScenarioValue2String(v[el]));
546 				else rval = Format("%s=%s", el, SaveScenarioValue2String(v[el]));
547 			}
548 			if (rval) rval = Format("{%s}", rval); else rval = "{}";
549 			return rval;
550 		}
551 	}
552 	// int as constant? (treat nil as 0 in this case)
553 	if (constant_prefix)
554 		if (allow_bitmask)
555 			return GetBitmaskNameByValue(v, constant_prefix);
556 		else
557 			return GetConstantNameByValueSafe(v, constant_prefix);
558 	// Strings need to be quoted and escaped
559 	if (vt == C4V_String)
560 	{
561 		return Format("\"%s\"", ReplaceString(ReplaceString(v, "\\", "\\\\"), "\"", "\\\""));
562 	}
563 	// Otherwise, rely on the default %v formatting
564 	return Format("%v", v);
565 }
566 
GetBitmaskNameByValue(v,prefix)567 global func GetBitmaskNameByValue(v, prefix)
568 {
569 	// Compose bitmask of names of individual bits
570 	// e.g. GetBitmaskNameByValue(3, "C4D_") == "C4D_StaticBack|C4D_Structure"
571 	var s, n=0;
572 	for (var i=0; i<31 && v; ++i)
573 	{
574 		var v2 = 1<<i;
575 		if (v & v2)
576 		{
577 			v &= ~v2;
578 			var s2 = GetConstantNameByValue(v2, prefix);
579 			if (s2) s2 = Format("%s%s", prefix, s2); else s2 = Format("%x", v2);
580 			if (s) s = Format("%s|%s", s, s2); else s = s2;
581 		}
582 	}
583 	return s ?? GetConstantNameByValueSafe(0, prefix);
584 }
585 
GetConstantNameByValueSafe(v,prefix)586 global func GetConstantNameByValueSafe(v, prefix)
587 {
588 	// Get constant name by value including prefix. If not found, return number as string
589 	var s = GetConstantNameByValue(v, prefix);
590 	if (s) return Format("%s%s", prefix, s); else return Format("%d", v);
591 }
592 
593 
594 /* SaveScen_PropList functions */
595 // I would like to use non-global here, but how can I take a pointer then?
596 
SaveScenP_Add(string name,string s,...)597 global func SaveScenP_Add(string name, string s, ...)
598 {
599 	// apply format parametrers
600 	s = Format(s, ...);
601 	// just append to array of strings
602 	// could build a string using data=Format("%s%s",data,s); here - however, then we'd be limited to some internal buffer sizes
603 	var new_data = {name=name, s=s};
604 	if (!this.data) this.data=[new_data]; else this.data[GetLength(this.data)] = new_data;
605 	return true;
606 }
607 
SaveScenP_Remove(string name)608 global func SaveScenP_Remove(string name)
609 {
610 	// return all lines identified by name. return number of lines removed
611 	var idx, n=0, n_data;
612 	if (this.data)
613 	{
614 		n_data = GetLength(this.data);
615 		while (true)
616 		{
617 			idx = -1;
618 			while (++idx<n_data) if (this.data[idx].name == name) break;
619 			if (idx == n_data) break;
620 			++n;
621 			if (!--n_data) { this.data=nil; break; }
622 			if (idx < n_data) this.data[idx] = this.data[n_data];
623 			SetLength(this.data, n_data);
624 		}
625 	}
626 	return n;
627 }
628 
SaveScenP_RemoveCreation()629 global func SaveScenP_RemoveCreation()
630 {
631 	// Remove any creation strings
632 	return this->Remove(SAVEOBJ_ContentsCreation) + this->Remove(SAVEOBJ_ContentsCreationEx) + this->Remove(SAVEOBJ_Creation);
633 }
634 
SaveScenP_Clear()635 global func SaveScenP_Clear()
636 {
637 	// Remove all property and creation data
638 	this.data = nil;
639 	return true;
640 }
641 
SaveScenP_AddCall(string name,proplist obj,string set_fn,...)642 global func SaveScenP_AddCall(string name, proplist obj, string set_fn, ...)
643 {
644 	// add string of style Obj123->SetFoo(bar, baz, ...)
645 	// string parameters will not be quoted, so the caller can do some parameter formatting
646 	// compose parameter string
647 	var max_pars = 10, last_written = 2, set_pars = "", n_pars = 0;
648 	for (var i=3; i<max_pars; ++i)
649 	{
650 		var par = Par(i);
651 		var par_type = GetType(par);
652 		if (par_type)
653 		{
654 			while (++last_written < i)
655 				if (n_pars++)
656 					set_pars = Format("%s%s", set_pars, ", nil");
657 				else
658 					set_pars = "nil";
659 			if (par_type != C4V_String) par = SaveScenarioValue2String(par);
660 			if (n_pars++)
661 				set_pars = Format("%s, %s", set_pars, par);
662 			else
663 				set_pars = par;
664 		}
665 	}
666 	// add setter
667 	return this->Add(name, "%s->%s(%s)", obj->MakeScenarioSaveName(), set_fn, set_pars);
668 }
669 
SaveScenP_AddSet(string name,object obj,string local_name,value)670 global func SaveScenP_AddSet(string name, object obj, string local_name, value)
671 {
672 	// add string of style Obj123->local_name = value
673 	// string parameters will not be quoted, so the caller can do some parameter formatting
674 	if (GetType(value) != C4V_String) value = SaveScenarioValue2String(value);
675 	return this->Add(name, "%s.%s = %s", obj->MakeScenarioSaveName(), local_name, value);
676 }
677 
SaveScenP_Buffer2File(f)678 global func SaveScenP_Buffer2File(f)
679 {
680 	// buffer.data is an array of strings to be written to file f
681 	if (!this.data) return false;
682 	var i=0, indent = "";
683 	for (var creation in [true, false])
684 	{
685 		for (var v in this.data)
686 		{
687 			var v_is_creation = ((v.name == SAVEOBJ_Creation) || (v.name == SAVEOBJ_ContentsCreation) || (v.name == SAVEOBJ_ContentsCreationEx));
688 			if (v_is_creation != creation) continue;
689 			if (i || !creation) indent = "	";
690 			FileWrite(f, Format("%s%s;\n", indent, v.s));
691 			++i;
692 		}
693 	}
694 	return i;
695 }
696 
SaveScenP_HasData()697 global func SaveScenP_HasData()
698 {
699 	// Anything added to data array?
700 	return !!this.data;
701 }
702 
SaveScenP_HasCreation()703 global func SaveScenP_HasCreation()
704 {
705 	// Functions to test whether any creation data has been added
706 	if (!this.data) return false;
707 	for (var v in this.data) if ((v.name == SAVEOBJ_Creation) || (v.name == SAVEOBJ_ContentsCreation) || (v.name == SAVEOBJ_ContentsCreationEx)) return true;
708 	return false;
709 }
710 
SaveScenP_HasProps()711 global func SaveScenP_HasProps()
712 {
713 	// Functions to test whether any property data has been added
714 	if (!this.data) return false;
715 	for (var v in this.data) if ((v.name != SAVEOBJ_Creation) && (v.name != SAVEOBJ_ContentsCreation) && (v.name != SAVEOBJ_ContentsCreationEx)) return true;
716 	return false;
717 }
718 
SaveScenP_HasProp(string prop)719 global func SaveScenP_HasProp(string prop)
720 {
721 	// Test if specific prop is present
722 	if (!this.data) return nil;
723 	for (var v in this.data) if (v.name == prop) return v;
724 	return nil;
725 }
726 
SaveScenP_TakeProps()727 global func SaveScenP_TakeProps()
728 {
729 	// Remove all props from this data and add them to a copy of this
730 	var result = { Prototype = this.Prototype };
731 	if (this.data)
732 	{
733 		var creation = nil, props = nil;
734 		for (var v in this.data)
735 			if ((v.name != SAVEOBJ_Creation) && (v.name != SAVEOBJ_ContentsCreation) && (v.name != SAVEOBJ_ContentsCreationEx))
736 				if (!props) props = [v]; else props[GetLength(props)] = v;
737 			else
738 				if (!creation) creation = [v]; else creation[GetLength(creation)] = v;
739 		this.data = creation;
740 		result.data = props;
741 		result.origin = this;
742 	}
743 	return result;
744 }
745 
CustomInitialize(string script)746 global func CustomInitialize(string script)
747 {
748 	// run a custom object initialization and
749 	return eval(this.CustomInitializationScript = script);
750 }
751