/** SaveScenario.c Scenario saving functionality @author Sven2 */ // Defines script function SaveScenarioObjects, which is called by the // engine to generate the Objects.c file for scenario saving // Also called for object duplication in the editor // Temp variables used by MakeScenarioSaveName() // These variables could be passed as a parameter through all saving functions instead. // But that would include every single SaveScenarioObject call and associated functions and would be easy for scripters to forget. static save_scenario_obj_dependencies; // Dependency graph to ensure objects are saved in proper order static save_scenario_def_indices; // Used to generate unique indices in variable names static save_scenario_dup_objects; // Objects to duplicate if SaveScenarioObjects is called for object duplication. // Propert identifier of object creation static const SAVEOBJ_Creation = "Creation"; static const SAVEOBJ_ContentsCreation = "ContentsCreation"; static const SAVEOBJ_ContentsCreationEx = "ContentsCreationEx"; global func SaveScenarioObjects(f, duplicate_objects) { // f is a handle to the Objects.c file // If called for object duplication, duplicate_objects is an array of objects to duplicate save_scenario_dup_objects = duplicate_objects; // Prepare props saving object var props_prototype = { Add = Global.SaveScenP_Add, AddSet = Global.SaveScenP_AddSet, AddCall = Global.SaveScenP_AddCall, Remove = Global.SaveScenP_Remove, RemoveCreation = Global.SaveScenP_RemoveCreation, Clear = Global.SaveScenP_Clear, Buffer2File = Global.SaveScenP_Buffer2File, HasData = Global.SaveScenP_HasData, HasCreation = Global.SaveScenP_HasCreation, HasProps = Global.SaveScenP_HasProps, HasProp = Global.SaveScenP_HasProp, TakeProps = Global.SaveScenP_TakeProps }; // Write all (scenario) or specified (duplication) objects! var objs = duplicate_objects, obj, i; if (!objs) objs = FindObjects(Find_And()); var n = GetLength(objs); var obj_type, any_written, do_write_file = false; save_scenario_def_indices = nil; // In reverse order (background to foreground) for (i=0; iGetID()) { if (any_written) spacing = "\n "; // Extra spacing between different object types obj_type = obj.o->GetID(); any_written = false; } if (obj.o.StaticSaveVar && !save_scenario_dup_objects) { if (obj.props->HasCreation()) FileWrite(f, Format("%s%s = ", spacing, obj.o.StaticSaveVar)); } else if (obj.write_label) { FileWrite(f, Format("%svar %s", spacing, obj.o->MakeScenarioSaveName())); if (obj.props->HasCreation()) FileWrite(f, " = "); else FileWrite(f, ";\n"); } else if (obj.props->HasCreation()) { FileWrite(f, spacing); } if (obj.props->~Buffer2File(f)) do_write_file = any_written = true; } // Write global effects if (!save_scenario_dup_objects) { any_written = false; var fx; i=0; while (fx = GetEffect("*", nil, i++)) { var fx_buffer = {Prototype=props_prototype}; EffectCall(nil, fx, "SaveScen", fx_buffer); if (fx_buffer->HasData()) { if (!any_written && do_write_file) FileWrite(f, " \n"); any_written = do_write_file = true; fx_buffer->~Buffer2File(f); } } } // Cleanup save_scenario_def_indices = save_scenario_obj_dependencies = save_scenario_dup_objects = nil; // Write footer FileWrite(f, " return true;\n}\n"); // Done; success. Return true if any objects or effects were written to the file. return do_write_file; } global func SaveScen_Objects(array objs, array ignore_objs, proplist props_prototype) { // Write all object data into buffers var n = GetLength(objs); var obj_data = CreateArray(n), obj; for (var i=0; i= 0) is_ignored = true; if (WildcardMatch(Format("%i", container_obj->GetID()), "GUI_*")) is_ignored = true; // big bunch of objects that should not be stored. container_obj = container_obj->Contained(); } if (is_ignored) continue; // Generate object creation and property strings save_scenario_obj_dependencies = []; if (!obj->SaveScenarioObject(obj_data[i].props)) { obj_data[i].props->Clear(); continue; } if (obj->Contained()) obj->Contained()->MakeScenarioSaveName(); // force container dependency if (obj_data[i].props->HasProps()) obj_data[i].write_label = true; obj_data[i].dependencies = save_scenario_obj_dependencies; obj_data[i].n_dependencies = GetLength(save_scenario_obj_dependencies); save_scenario_obj_dependencies = nil; } return obj_data; } global func SaveScen_ResolveDepends(array objs, array obj_data) { // Dependency pointer from obj to obj_data var i,j,k,n=GetLength(objs),od; for (i=0; iContained()) { k = GetIndexOf(objs, objs[i]->Contained()); if (k >= 0) obj_data[i].co = obj_data[k]; } } // Resolve dependencies k = 0; for (i=0; i i) { // The dependent object is behind us in the list. This is bad! if (!od.props->HasCreation()) { // This is just object properties. Move them behind dependent object creation var obj_data_new = CreateArray(n); obj_data_new[0:i] = obj_data[0:i]; obj_data_new[i:j] = obj_data[i+1:j+1]; obj_data_new[j] = obj_data[i]; if (jTakeProps() }; od.n_dependencies = 0; od.props_detached = true; if (jContained()) { // ignore if container object was not saved if (obj.co && obj.co.props->HasCreation()) { // creation and props? Then turn into CreateContents if (obj.props->HasProp(SAVEOBJ_ContentsCreation) && !obj.props_detached) { obj.props->Remove(SAVEOBJ_Creation); // Adjust owner if necessery if (obj.o->GetOwner() != obj.o->Contained()->GetOwner()) obj.props->AddCall("Owner", obj.o, "SetOwner", obj.o->GetOwner()); } else if (obj.props.origin) { // props detached from creation. Use Enter() call to enter container // the label must have been written because something depended on the object. obj.props.origin->Remove(SAVEOBJ_ContentsCreation); obj.props.origin->Remove(SAVEOBJ_ContentsCreationEx); obj.props->AddCall("Container", obj.o, "Enter", obj.o->Contained()); // Ensure layer is written for detached contents if it is the same as the container var o_layer = obj.o->GetObjectLayer(); if (o_layer && o_layer == obj.co->GetObjectLayer()) obj.props->AddCall("Layer", obj.o, "SetObjectLayer", o_layer); } } else { // unsaved container - just create object outside obj.props->Remove(SAVEOBJ_ContentsCreation); obj.props->Remove(SAVEOBJ_ContentsCreationEx); } } // Concatenate multiple contents creations for (var obj in obj_data) if (obj.o->Contained() && obj.props->HasProp(SAVEOBJ_ContentsCreationEx)) { var num_contents_concat = 1; if ((!obj.o.StaticSaveVar || save_scenario_dup_objects) && !obj.write_label) { for (var obj2 in obj_data) if (obj2 != obj && obj2.o->Contained() == obj.o->Contained() && obj.o->GetID() == obj2.o->GetID() && obj2.props->HasProp(SAVEOBJ_ContentsCreationEx)) { ++num_contents_concat; obj2.props->Clear(); } } if (num_contents_concat > 1) { obj.props->Remove(SAVEOBJ_ContentsCreation); var creation_prop = obj.props->HasProp(SAVEOBJ_ContentsCreationEx); creation_prop.s = Format(creation_prop.s, num_contents_concat); } else obj.props->Remove(SAVEOBJ_ContentsCreationEx); } return obj_data; } global func AddScenarioSaveDependency() { // Remember this object in the list of dependencies for the currently processed object if (save_scenario_obj_dependencies && GetIndexOf(save_scenario_obj_dependencies, this)<0) save_scenario_obj_dependencies[GetLength(save_scenario_obj_dependencies)] = this; } // documented in /docs/sdk/script/fn global func MakeScenarioSaveName() { // Get name to be used to store this object in a scenario if (!this) FatalError("MakeScenarioSaveName needs definition or object context!"); // Definitions may just use their regular name if (this.Prototype == Global) return Format("%i", this); // Duplication mode: If this is an object that is not being duplicated, just reference it as Object(number) if (save_scenario_dup_objects && GetIndexOf(save_scenario_dup_objects, this)<0) return Format("%v", this); // When the name is queried while properties are built, it means that there is a dependency. Store it. AddScenarioSaveDependency(); // Write name if it had been used elsewhere if (this._save_scen_objdata) this._save_scen_objdata.write_label = true; // Build actual name using unique number (unless there's a static save variable name for us) if (this.StaticSaveVar && !save_scenario_dup_objects) return this.StaticSaveVar; if (!save_scenario_def_indices) save_scenario_def_indices = {}; var base_name = Format("%i", GetID()); if (base_name == "") base_name = "Unknown"; // save_scenario_def_indices is a proplist containing arrays of all objects sorted by type // the saved name is , where index is the 1-based index into the array var def_indices = save_scenario_def_indices[base_name]; var idx; if (!def_indices) { save_scenario_def_indices[base_name] = def_indices = [this]; idx = 0; } else { idx = GetIndexOf(def_indices, this); if (idx<0) { idx = GetLength(def_indices); def_indices[idx] = this; } } return Format("%s%03d", base_name, idx+1); } global func SaveScenarioObject(props) { // Called in object context: Default object writing procedure // Overwrite this method and return false for objects that should not be saved // Overwrite and call inherited for objects that add/remove/alter default creation/properties var owner_string = "", i; if (GetOwner() != NO_OWNER) owner_string = Format(", %d", GetOwner()); // Object creation: Default is to create above bottom center point // This usually works well with stuff like buildings that may change size in updated versions // 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 var is_centered_creation = (!this.SaveScenarioCreateFromBottom) && (GetR() || Contained() || GBackSolid() || (GetCategory() & (C4D_Rule | C4D_Goal | C4D_Environment)) || this.SaveScenarioCreateCentered); if (is_centered_creation) { if (!GetX() && !GetY() && (owner_string == "")) props->Add(SAVEOBJ_Creation, "CreateObject(%i)", GetID()); // super-short version for e.g. goals/rules at position 0/0 else props->Add(SAVEOBJ_Creation, "CreateObject(%i, %d, %d%s)", GetID(), GetX(), GetY(), owner_string); } else { props->Add(SAVEOBJ_Creation, "CreateObjectAbove(%i, %d, %d%s)", GetID(), GetX(), GetDefBottom(), owner_string); } // Contained creation is added alongside regular creation because it is not yet known if CreateObject+Enter or CreateContents can be used due to dependencies. // func SaveScen_SetContainers will take care of removing one of the two creation strings after dependencies have been resolved. if (Contained()) { props->Add(SAVEOBJ_ContentsCreation, "%s->CreateContents(%i)", Contained()->MakeScenarioSaveName(), GetID()); props->Add(SAVEOBJ_ContentsCreationEx, "%s->CreateContents(%i, %%d)", Contained()->MakeScenarioSaveName(), GetID()); } // Write some default props every object should save var v, is_static = (GetCategory() & C4D_StaticBack) || Contained(), def = GetID(); v = GetAlive(); if (!v && (GetCategory()&C4D_Living)) props->AddCall("Alive", this, "Kill", this, true); v = GetDir(); if (v) props->AddCall("Dir", this, "SetDir", GetConstantNameByValueSafe(v,"DIR_")); v = GetComDir(); if (v) props->AddCall("ComDir", this, "SetComDir", GetConstantNameByValueSafe(v,"COMD_")); v = GetCon(); if (v != 100) props->AddCall("Con", this, "SetCon", Max(v,1)); v = GetCategory(); if (v != def->GetCategory()) props->AddCall("Category", this, "SetCategory", GetBitmaskNameByValue(v, "C4D_")); v = GetR(); if (v && !Contained()) props->AddCall("R", this, "SetR", v); v = GetXDir(); if (v && !is_static) props->AddCall("XDir", this, "SetXDir", v); v = GetYDir(); if (v && !is_static) if (!Inside(v, 1,12) || !GetContact(-1, CNAT_Bottom)) props->AddCall("YDir", this, "SetYDir", v); // consolidate small YDir for standing objects v = GetRDir(); if (v && !is_static) props->AddCall("RDir", this, "SetRDir", v); var default_color = 0xffffffff; if (GetDefColorByOwner()) if (GetOwner() == NO_OWNER) default_color = 0xff; else default_color = GetPlayerColor(GetOwner()); v = GetColor(); if (v && v != default_color) props->AddCall("Color", this, "SetColor", Format("0x%x", v)); v = GetClrModulation(); if (v && v != 0xffffffff) props->AddCall("ClrModulation", this, "SetClrModulation", Format("0x%08x", v)); v = GetObjectBlitMode();if (v) props->AddCall("BlitMode", this, "SetObjectBlitMode", GetBitmaskNameByValue(v & ~GFX_BLIT_Custom, "GFX_BLIT_")); for (i=0; v=def->GetMeshMaterial(i); ++i) if (GetMeshMaterial(i) != v) props->AddCall("MeshMaterial", this, "SetMeshMaterial", Format("%v", GetMeshMaterial(i)), i); v = this.Name; if (v != def.Name) props->AddCall("Name", this, "SetName", SaveScenarioValue2String(v)); v = this.MaxEnergy; if (v != def.MaxEnergy) props->AddSet ("MaxEnergy", this, "MaxEnergy", this.MaxEnergy); v = GetEnergy(); if (v != def.MaxEnergy/1000) props->AddCall("Energy", this, "DoEnergy", v-def.MaxEnergy/1000); v = this.Visibility; if (v != def.Visibility) props->AddSet ("Visibility", this, "Visibility", SaveScenarioValue2String(v, "VIS_", true)); v = this.Plane; if (v != def.Plane) props->AddSet ("Plane", this, "Plane", v); v = GetObjectLayer(); var def_layer=nil; if (Contained()) def_layer = Contained()->GetObjectLayer(); if (v != def_layer) props->AddCall("Layer", this, "SetObjectLayer", v); v = this.LineColors; if (v != def.LineColors) props->AddSet ("LineColors", this, "LineColors", v); 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 // Commands: Could store the whole command stack using AppendCommand. // However, usually there is one base command and the rest is derived // (e.g.: A Get command may lead to multiple MoveTo commands to the // target object). So just store the topmost command. var command, last_command; i=0; while (command = GetCommand(0, i++)) last_command = command; if (last_command) { i-=2; props->AddCall("Command", this, "SetCommand", Format("%v", last_command), SaveScenarioValue2String(GetCommand(1,i)), // target SaveScenarioValue2String(GetCommand(2,i)), // x SaveScenarioValue2String(GetCommand(3,i)), // y SaveScenarioValue2String(GetCommand(4,i)), // target2 SaveScenarioValue2String(GetCommand(5,i))); // data } // Effects var fx; i=0; while (fx = GetEffect("*", this, i++)) EffectCall(this, fx, "SaveScen", props); // EditorProps if (this.EditorProps) { var all_prop_names = GetProperties(this.EditorProps), prop_name, prop; for (prop_name in all_prop_names) { if ((prop = this.EditorProps[prop_name]) != nil) { if (GetType(prop) == C4V_PropList) { if (prop.Save) { v = this[prop_name]; var default_v = GetID()[prop_name]; if (!DeepEqual(v, default_v)) { if (prop.Set) { // Save as call props->AddCall(prop.Save, this, prop.Set, SaveScenarioValue2String(v)); } else { // Save as direct value setting props->AddSet(prop.Save, this, prop_name, SaveScenarioValue2String(v)); } } } } } } } // 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. if (is_centered_creation && (GetCon != 100 || (GetR() && !Contained()))) props->AddCall("SetPosition", this, "SetPosition", GetX(), GetY()); // Automatic unsticking for items lying on the ground and for animals / clonks // Do this after Con/Rotation and other calls that may affect the shape // (Note: If someone loads a game in paused mode and immediately saves without unpausing, most unstick calls for items will be lost) if (!Contained() && !this.SaveScenarioCreateCentered && !this.SaveScenarioCreateFromBottom && !Stuck() && (GetAlive() || (this.Collectible && GetContact(-1, CNAT_Left | CNAT_Right | CNAT_Top | CNAT_Bottom)))) { 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] props->AddCall("Unstick", this, "Unstick", unstick_range); } // Initialization function as late as possible v = this.CustomInitializationScript; if (v) props->AddCall("CustomInitialization", this, "CustomInitialize", Format("%v", v)); return true; } // documented in /docs/sdk/script/fn global func SaveScenarioObjectAction(props) { // Helper function to store action properties props->AddCall("Action", this, "SetAction", Format("%v", GetAction() ?? "Idle"), GetActionTarget(), GetActionTarget(1)); var v = GetPhase(); if (v) props->AddCall("Phase", this, "SetPhase", v); return true; } global func FxFireSaveScen(object obj, proplist fx, proplist props) { // this is burning. Save incineration to scenario. props->AddCall("Fire", obj, "Incinerate", fx.strength, fx.caused_by, fx.blasted, fx.incinerating_object); return true; } /* Helper functions for value formatting */ // Helper function to turn values of several types into a strings to be written to Objects.c global func SaveScenarioValue2String(v, string constant_prefix, bool allow_bitmask) { var rval, vt = GetType(v); if (vt == C4V_C4Object) return v->MakeScenarioSaveName(); if (vt == C4V_Array) // save procedure for arrays: recurse into contents (cannot save arrays pointing into itself that way) { for (var el in v) { if (rval) rval = Format("%s,%s", rval, SaveScenarioValue2String(el, constant_prefix, allow_bitmask)); else rval = SaveScenarioValue2String(el, constant_prefix, allow_bitmask); constant_prefix = nil; // Only first element is actual bitmask (at least or VIS_, and that's the only user for this case) } if (rval) rval = Format("[%s]", rval); else rval = "[]"; return rval; } // custom save procedure for some prop lists or definitions if (vt == C4V_PropList || vt == C4V_Def) { rval = v->~ToString(); if (rval) return rval; // proplist saving if (vt == C4V_PropList) { var props = GetProperties(v); for (var el in props) { if (GetChar(el) == GetChar("_")) continue; // props starting with underscore are not to be saved if (rval) rval = Format("%s,%s=%s", rval, el, SaveScenarioValue2String(v[el])); else rval = Format("%s=%s", el, SaveScenarioValue2String(v[el])); } if (rval) rval = Format("{%s}", rval); else rval = "{}"; return rval; } } // int as constant? (treat nil as 0 in this case) if (constant_prefix) if (allow_bitmask) return GetBitmaskNameByValue(v, constant_prefix); else return GetConstantNameByValueSafe(v, constant_prefix); // Strings need to be quoted and escaped if (vt == C4V_String) { return Format("\"%s\"", ReplaceString(ReplaceString(v, "\\", "\\\\"), "\"", "\\\"")); } // Otherwise, rely on the default %v formatting return Format("%v", v); } global func GetBitmaskNameByValue(v, prefix) { // Compose bitmask of names of individual bits // e.g. GetBitmaskNameByValue(3, "C4D_") == "C4D_StaticBack|C4D_Structure" var s; for (var i=0; i<31 && v; ++i) { var v2 = 1<Remove(SAVEOBJ_ContentsCreation) + this->Remove(SAVEOBJ_ContentsCreationEx) + this->Remove(SAVEOBJ_Creation); } global func SaveScenP_Clear() { // Remove all property and creation data this.data = nil; return true; } global func SaveScenP_AddCall(string name, proplist obj, string set_fn, ...) { // add string of style Obj123->SetFoo(bar, baz, ...) // string parameters will not be quoted, so the caller can do some parameter formatting // compose parameter string var max_pars = 10, last_written = 2, set_pars = "", n_pars = 0; for (var i=3; iAdd(name, "%s->%s(%s)", obj->MakeScenarioSaveName(), set_fn, set_pars); } global func SaveScenP_AddSet(string name, object obj, string local_name, value) { // add string of style Obj123->local_name = value // string parameters will not be quoted, so the caller can do some parameter formatting if (GetType(value) != C4V_String) value = SaveScenarioValue2String(value); return this->Add(name, "%s.%s = %s", obj->MakeScenarioSaveName(), local_name, value); } global func SaveScenP_Buffer2File(f) { // buffer.data is an array of strings to be written to file f if (!this.data) return false; var i=0, indent = ""; for (var creation in [true, false]) { for (var v in this.data) { var v_is_creation = ((v.name == SAVEOBJ_Creation) || (v.name == SAVEOBJ_ContentsCreation) || (v.name == SAVEOBJ_ContentsCreationEx)); if (v_is_creation != creation) continue; if (i || !creation) indent = " "; FileWrite(f, Format("%s%s;\n", indent, v.s)); ++i; } } return i; } global func SaveScenP_HasData() { // Anything added to data array? return !!this.data; } global func SaveScenP_HasCreation() { // Functions to test whether any creation data has been added if (!this.data) return false; for (var v in this.data) if ((v.name == SAVEOBJ_Creation) || (v.name == SAVEOBJ_ContentsCreation) || (v.name == SAVEOBJ_ContentsCreationEx)) return true; return false; } global func SaveScenP_HasProps() { // Functions to test whether any property data has been added if (!this.data) return false; for (var v in this.data) if ((v.name != SAVEOBJ_Creation) && (v.name != SAVEOBJ_ContentsCreation) && (v.name != SAVEOBJ_ContentsCreationEx)) return true; return false; } global func SaveScenP_HasProp(string prop) { // Test if specific prop is present if (!this.data) return nil; for (var v in this.data) if (v.name == prop) return v; return nil; } global func SaveScenP_TakeProps() { // Remove all props from this data and add them to a copy of this var result = { Prototype = this.Prototype }; if (this.data) { var creation = nil, props = nil; for (var v in this.data) if ((v.name != SAVEOBJ_Creation) && (v.name != SAVEOBJ_ContentsCreation) && (v.name != SAVEOBJ_ContentsCreationEx)) if (!props) props = [v]; else props[GetLength(props)] = v; else if (!creation) creation = [v]; else creation[GetLength(creation)] = v; this.data = creation; result.data = props; result.origin = this; } return result; } global func CustomInitialize(string script) { // run a custom object initialization and return eval(this.CustomInitializationScript = script); }