diff --git a/DuplicateFinder/info.toml b/DuplicateFinder/info.toml new file mode 100644 index 0000000..cc98b62 --- /dev/null +++ b/DuplicateFinder/info.toml @@ -0,0 +1,9 @@ +[meta] +name = "DuplicateFinder" +author = "Beu" +category = "Map Editor" +version = "1.1" +siteid = 220 + +[script] +timeout = 0 diff --git a/DuplicateFinder/main.as b/DuplicateFinder/main.as new file mode 100644 index 0000000..d479c0a --- /dev/null +++ b/DuplicateFinder/main.as @@ -0,0 +1,303 @@ +class Objects { //Items or Blocks + string name; + string type; + int count; + vec3 position; + vec3 rotation; + Objects(string name, string type, vec3 pos, vec3 rot ) { + this.name = name; + this.type = type; + this.count = 2; // Object if added only if duplicate + this.position = pos; + this.rotation = rot; + } +} + +enum ESortColumn { + ItemName, + Count +} + +const float pi = 3.14159265359; + +bool include_default_blocks; +bool menu_visibility = false; +bool refreshobject; + +bool sort_reverse; +bool forcesort; +string infotext; + +array objects = {}; +array sortableobjects = {}; +array objectsindex = {}; + +int totalobjects; +int computedobjects; + +ESortColumn sortCol = ESortColumn(-1); + +void Main() { + while (true) { + if (refreshobject) { + objects.Resize(0); + objectsindex.Resize(0); + sortableobjects.Resize(0); + RefreshBlocksAndItems(); + sortableobjects = objects; + refreshobject = false; + } + yield(); + } +} + +// Force to split the refresh functions to bypass the script execution delay on heavy maps +void RefreshBlocksAndItems() { + auto map = GetApp().RootMap; + dictionary singleobjectdico = {}; + + if (map !is null) { + totalobjects = map.Blocks.Length + map.AnchoredObjects.Length; + computedobjects = 0; + + // Blocks + auto blocks = map.Blocks; + + // Editor plugin API for GetVec3FromCoord function + auto pluginmaptype = cast(cast(GetApp().Editor).PluginMapType); + + for(uint i = 0; i < blocks.Length; i++) { + if (i % 1000 == 0) yield(); // to avoid timeout + computedobjects++; + + string blockname; + blockname = blocks[i].BlockModel.IdName; + if (blockname.ToLower().SubStr(blockname.Length - 22, 22) == ".block.gbx_customblock") blockname = blockname.SubStr(0, blockname.Length - 12); + + vec3 pos; + vec3 rot; + if (blocks[i].CoordX != 4294967295 && blocks[i].CoordZ != 4294967295) { // Not placed in free mapping + if (!include_default_blocks) continue; + if (pluginmaptype !is null) { // Editor plugin is available + pos = pluginmaptype.GetVec3FromCoord(blocks[i].Coord); + } else { + pos.x = blocks[i].CoordX * 32; + pos.y = (blocks[i].CoordY - 8) * 8; + pos.z = blocks[i].CoordZ * 32; + } + switch(blocks[i].BlockDir) { + case CGameCtnBlock::ECardinalDirections::East: + rot.x = 90; + break; + case CGameCtnBlock::ECardinalDirections::South: + rot.x = 180; + break; + case CGameCtnBlock::ECardinalDirections::West: + rot.x = 270; + break; + } + + } else { + uint16 FreeBlockPosOffset = Reflection::GetType("CGameCtnBlock").GetMember("Dir").Offset + 0x8; + uint16 FreeBlockRotOffset = FreeBlockPosOffset + 0xC; + + pos = Dev::GetOffsetVec3(blocks[i], FreeBlockPosOffset); + rot = Dev::GetOffsetVec3(blocks[i], FreeBlockRotOffset) / pi * 180; + } + + string uniqueid = pos.x + ";" + pos.y + ";" + pos.z + ";;" + rot.x + ";" + rot.y + ";" + rot.z + ";;"+ blockname; + + if (singleobjectdico.Exists(uniqueid)) { + int index = objectsindex.Find(uniqueid); + if (index >= 0) { + objects[index].count++; + } else { + objects.InsertLast(Objects(blockname, "Block", pos, rot)); + objectsindex.InsertLast(uniqueid); + } + } else { + singleobjectdico.Set(uniqueid, 0); + } + } + singleobjectdico.DeleteAll(); + + auto items = map.AnchoredObjects; + + for(uint i = 0; i < items.Length; i++) { + if (i % 1000 == 0) yield(); // to avoid timeout + computedobjects++; + + string itemname = items[i].ItemModel.IdName; + + vec3 pos = items[i].AbsolutePositionInMap; + vec3 rot; + rot.x = items[i].Yaw; + rot.y = items[i].Pitch; + rot.z = items[i].Roll; + + string uniqueid = pos.x + ";" + pos.y + ";" + pos.z + ";;" + rot.x + ";" + rot.y + ";" + rot.z + ";;"+ itemname; + + if (singleobjectdico.Exists(uniqueid)) { + int index = objectsindex.Find(uniqueid); + if (index >= 0) { + objects[index].count++; + } else { + objects.InsertLast(Objects(itemname, "Item", pos, rot)); + objectsindex.InsertLast(uniqueid); + } + } else { + singleobjectdico.Set(uniqueid, 0); + } + } + } +} + +bool FocusCam(vec3 position) { + auto editor = cast(GetApp().Editor); + auto camera = editor.OrbitalCameraControl; + auto map = GetApp().RootMap; + + + if (camera !is null) { + camera.m_TargetedPosition = position; + // Workaround to update camera TargetedPosition + auto m_ParamScrollZoomPowe = camera.m_ParamScrollZoomPower; + camera.m_ParamScrollZoomPower = 0; + editor.ButtonZoomInOnClick(); + camera.m_ParamScrollZoomPower = m_ParamScrollZoomPowe; + + return true; + } + return false; +} + +void GenerateRow(Objects@ object) { + if (object.count <= 1) return; + UI::TableNextRow(); + UI::TableNextColumn(); + if (UI::Button(Icons::Search + "###Search;" + object.position.x + ";" + object.position.y + ";" + object.position.z)) { + FocusCam(object.position); + } + if (UI::IsItemHovered() && cast(cast(GetApp().Editor).PluginMapType) is null) infotext = "Editor plugins are disabled, the coordinates of the blocks are estimated and can be imprecise"; + UI::SameLine(); + UI::Text(object.name); + UI::TableNextColumn(); + UI::Text(object.type); + UI::TableNextColumn(); + UI::Text("<" + object.position.x + ", " + object.position.y + ", " + object.position.z +">"); + UI::TableNextColumn(); + UI::Text("<" + object.rotation.x + ", " + object.rotation.y + ", " + object.rotation.z +">"); + UI::TableNextColumn(); + UI::Text(Text::Format("%lld",object.count)); +} + +void Render() { + if (!menu_visibility) { + return; + } + + CGameCtnEditorFree@ editor = cast(GetApp().Editor); + CGameCtnChallenge@ map = cast(GetApp().RootMap); + + if (map is null && editor is null) { + menu_visibility = false; + return; + } + + infotext = ""; + + UI::SetNextWindowSize(600, 400); + UI::SetNextWindowPos(200, 200, UI::Cond::Once); + UI::Begin("\\$cf9" + Icons::Table + "\\$z DuplicateFinder###DuplicateFinder", menu_visibility); + if (editor !is null) { + if (refreshobject) { + if (Time::get_Now() % 3000 > 2000) { + UI::Button("Loading..."); + } else if (Time::get_Now() % 3000 > 1000) { + UI::Button("Loading.. "); + } else { + UI::Button("Loading. "); + } + if (UI::IsItemHovered()) infotext = "Parsing all blocks and items to generate the table. Please wait... (" + computedobjects + "/" + totalobjects + ")"; + } else { + if (UI::Button(Icons::SyncAlt + " Refresh")) { + refreshobject = true; + forcesort = true; + } + } + UI::SameLine(); + include_default_blocks = UI::Checkbox("Include Blocks not placed in Free Mapping", include_default_blocks); + UI::Separator(); + vec2 winsize = UI::GetWindowSize(); + winsize.x = winsize.x - 10; + winsize.y = winsize.y - 105; + if (UI::BeginTable("DuplicateBlocksTable", 5, UI::TableFlags(UI::TableFlags::Resizable | UI::TableFlags::Sortable | UI::TableFlags::NoSavedSettings | UI::TableFlags::BordersInnerV | UI::TableFlags::SizingStretchProp | UI::TableFlags::ScrollY),winsize )) { + UI::TableSetupScrollFreeze(0, 1); + UI::TableSetupColumn("Block Name", UI::TableColumnFlags::None, 40.f, ESortColumn::ItemName); + UI::TableSetupColumn("Type", UI::TableColumnFlags::NoSort, 10.f); + UI::TableSetupColumn("Position", UI::TableColumnFlags::NoSort, 20.f); + UI::TableSetupColumn("Rotation", UI::TableColumnFlags::NoSort, 20.f); + UI::TableSetupColumn("Count", UI::TableColumnFlags::DefaultSort, 10.f, ESortColumn::Count); + UI::TableHeadersRow(); + + UI::TableSortSpecs@ sortSpecs = UI::TableGetSortSpecs(); + if(sortSpecs !is null && sortSpecs.Specs.Length == 1 && sortableobjects.Length > 1) { + if(sortSpecs.Dirty || (forcesort && !refreshobject)) { + if(sortCol != ESortColumn(sortSpecs.Specs[0].ColumnUserID) || (forcesort && !refreshobject)) { + sortCol = ESortColumn(sortSpecs.Specs[0].ColumnUserID); + switch(sortCol) { + case ESortColumn::ItemName: + sortableobjects.Sort(function(a,b) { return a.name < b.name; }); + break; + case ESortColumn::Count: + sortableobjects.Sort(function(a,b) { return a.count < b.count; }); + break; + } + if (forcesort && sort_reverse) { + sortableobjects.Reverse(); + } else { + sort_reverse = false; + } + } else if (sortCol == ESortColumn(sortSpecs.Specs[0].ColumnUserID)) { + sortableobjects.Reverse(); + sort_reverse = !sort_reverse; + } + + sortSpecs.Dirty = false; + forcesort = false; + } + } + if (sortableobjects.Length > 0 ) { + for(uint i = 0; i < sortableobjects.Length; i++) { + GenerateRow(sortableobjects[i]); + } + } else { + for(uint i = 0; i < objects.Length; i++) { + GenerateRow(objects[i]); + } + } + UI::EndTable(); + UI::Separator(); + UI::Text(Icons::Info + " " + infotext); + } + } else { + UI::Text("Open this plugin in the map editor"); + } + UI::End(); + +} + +void RenderMenu() { + CGameCtnEditorFree@ editor = cast(GetApp().Editor); + CGameCtnChallenge@ map = cast(GetApp().RootMap); + + if (map is null && editor is null) { + return; + } + + if(UI::MenuItem("\\$cf9" + Icons::Table + "\\$z DuplicateFinder", "", menu_visibility)) { + menu_visibility = !menu_visibility; + refreshobject = true; + } +} +