diff --git a/BlocksItemsCounter/Source/BlocksItemsCounter.as b/BlocksItemsCounter/Source/BlocksItemsCounter.as index a2839a4..78d4763 100644 --- a/BlocksItemsCounter/Source/BlocksItemsCounter.as +++ b/BlocksItemsCounter/Source/BlocksItemsCounter.as @@ -1,25 +1,3 @@ -class Objects { //Items or Blocks - string name; - int trigger; // CGameItemModel::EnumWaypointType or CGameCtnBlockInfo::EWayPointType - string type; - string source; - int size; - int count; - bool icon; - array positions; - - Objects(string name, int trigger, bool icon, string type, string source, int size, vec3 pos ) { - this.name = name; - this.trigger = trigger; - this.count = 1; - this.type = type; - this.icon = icon; - this.source = source; - this.size = size; - this.positions = {pos}; - } -} - enum ESortColumn { ItemName, Type, @@ -28,12 +6,13 @@ enum ESortColumn { Count } - bool menu_visibility = false; uint camerafocusindex = 0; bool include_default_objects = false; bool refreshobject; +string searchStr = ""; + bool sort_reverse; bool forcesort; string infotext; @@ -54,275 +33,64 @@ void Main() { RefreshBlocks(); RefreshItems(); sortableobjects = objects; + sortableobjects.Sort(function(a,b) { return a.size > b.size; }); // Sort by size by default, it will be used as second sort criteria refreshobject = false; } yield(); } } -// Force to split the refresh functions to bypass the script execution delay on heavy maps -void RefreshBlocks() { - auto map = GetApp().RootMap; - - if (map !is null) { - // 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++) { - int idifexist = -1; - string blockname; - bool isofficial = true; - blockname = blocks[i].BlockModel.IdName; - if (blockname.ToLower().SubStr(blockname.Length - 22, 22) == ".block.gbx_customblock") { - isofficial = false; - blockname = blockname.SubStr(0, blockname.Length - 12); - } - if (include_default_objects || blockname.ToLower().SubStr(blockname.Length - 10, 10) == ".block.gbx") { - vec3 pos; - if (blocks[i].CoordX != 4294967295 && blocks[i].CoordZ != 4294967295) { // Not placed in free mapping - if (pluginmaptype !is null) { // Editor plugin is available - pos = pluginmaptype.GetVec3FromCoord(blocks[i].Coord); - } else { - pos.x = blocks[i].CoordX * 32 + 16; - pos.y = (blocks[i].CoordY - 8) * 8 + 4; - pos.z = blocks[i].CoordZ * 32 + 16; - } - } else { - pos = Dev::GetOffsetVec3(blocks[i], 0x6c); - // center the coordinates in the middle of the block - pos.x += 16; - pos.y += 4; - pos.z += 16; - } - - - int index = objectsindex.Find(blockname); - - if (index >= 0) { - objects[index].count++; - objects[index].positions.InsertLast(pos); - } else { - int trigger = blocks[i].BlockModel.EdWaypointType; - AddNewObject(blockname, trigger, "Block", pos, 0, isofficial); - objectsindex.InsertLast(blockname); - } - } - if (i % 100 == 0) yield(); // to avoid timeout - } - } -} - -// Force to split the refresh functions to bypass the script execution delay on heavy maps -void RefreshItems() { - auto map = GetApp().RootMap; - - if (map !is null) { - // Items - auto items = map.AnchoredObjects; - for(uint i = 0; i < items.Length; i++) { - int idifexist = -1; - string itemname = items[i].ItemModel.IdName; - int fallbacksize = 0; - bool isofficial = true; - - if (itemname.ToLower().SubStr(itemname.Length - 9, 9) == ".item.gbx") { - isofficial = false; - auto article = cast(items[i].ItemModel.ArticlePtr); - if (article !is null) { - itemname = string(article.PageName) + string(article.Name) + ".Item.Gbx"; - } else { - auto fid = cast(GetFidFromNod(items[i].ItemModel)); - fallbacksize = fid.ByteSize; - } - } - - if (include_default_objects || itemname.ToLower().SubStr(itemname.Length - 9, 9) == ".item.gbx") { - int index = objectsindex.Find(itemname); - if (index >= 0) { - objects[index].count++; - objects[index].positions.InsertLast(items[i].AbsolutePositionInMap); - } else { - int trigger = items[i].ItemModel.WaypointType; - AddNewObject(itemname, trigger, "Item", items[i].AbsolutePositionInMap, fallbacksize, isofficial); - objectsindex.InsertLast(itemname); - } - } - if (i % 100 == 0) yield(); // to avoid timeout - } - } -} - -void AddNewObject(string objectname, int trigger, string type, vec3 pos, int fallbacksize, bool isofficial) { - bool icon = false; - int size; - string source; - CSystemFidFile@ file; - CGameCtnCollector@ collector; - CSystemFidFile@ tempfile; - - if (type == "Item" && Regex::IsMatch(objectname, "^[0-9]*/.*.zip/.*", Regex::Flags::None)) {// ItemCollections - source = "Club"; - @file = Fids::GetFake('MemoryTemp\\FavoriteClubItems\\' + objectname); - @collector = cast(cast(file.Nod)); - if (collector is null || (collector.Icon !is null || file.ByteSize == 0)) { - @tempfile = Fids::GetFake('MemoryTemp\\CurrentMap_EmbeddedFiles\\ContentLoaded\\ClubItems\\' + objectname); - } - } else { // Blocks and Items - source = "Local"; - @file = Fids::GetUser(type + 's\\' + objectname); - @collector = cast(cast(file.Nod)); - if (collector is null || (collector.Icon !is null || file.ByteSize == 0)) { - @tempfile = Fids::GetFake('MemoryTemp\\CurrentMap_EmbeddedFiles\\ContentLoaded\\' + type + 's\\' + objectname); - } - } - if (tempfile !is null) { - if (collector !is null && collector.Icon !is null && tempfile.ByteSize == 0) { - icon = true; - size = file.ByteSize; - } else { - size = tempfile.ByteSize; - } - if (isofficial) { - source = "In-Game"; - } else if (file.ByteSize == 0 && tempfile.ByteSize == 0 && fallbacksize == 0) { -#if TMNEXT - source = "Local"; -#else - source = "In TP"; -#endif - } else if (file.ByteSize == 0 && tempfile.ByteSize == 0 && fallbacksize > 0 ) { - source = "Embedded"; - size = fallbacksize; - } else if (file.ByteSize == 0 && tempfile.ByteSize > 0) { - source = "Embedded"; - } - } else { - size = file.ByteSize; - } - - objects.InsertLast(Objects(objectname, trigger, icon, type, source, size, pos)); -} - -bool FocusCam(string objectname) { - auto editor = cast(GetApp().Editor); - auto camera = editor.OrbitalCameraControl; - auto map = GetApp().RootMap; - - - if (camera !is null) { - int index = objectsindex.Find(objectname); - - camerafocusindex++; - - if (camerafocusindex > objects[index].positions.get_Length() - 1 ) { - camerafocusindex = 0; - } - - camera.m_TargetedPosition = objects[index].positions[camerafocusindex]; - // Workaround to update camera TargetedPosition - editor.ButtonZoomInOnClick(); - editor.ButtonZoomOutOnClick(); - return true; - } - return false; -} - -void GenerateRow(Objects@ object) { - UI::TableNextRow(); - UI::TableNextColumn(); - if (UI::Button(Icons::Search + "###" + object.name)) { - FocusCam(object.name); - } - if (UI::IsItemHovered() && object.type == "Block" && 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(); - switch(object.trigger){ - case CGameCtnBlockInfo::EWayPointType::Start: - UI::Text("\\$9f9" + object.name); - if (UI::IsItemHovered()) infotext = "It's a start block/item"; - break; - case CGameCtnBlockInfo::EWayPointType::Finish: - UI::Text("\\$f66" + object.name); - if (UI::IsItemHovered()) infotext = "It's a finish block/item"; - break; - case CGameCtnBlockInfo::EWayPointType::Checkpoint: - UI::Text("\\$99f" + object.name); - if (UI::IsItemHovered()) infotext = "It's a checkpoint block/item"; - break; - case CGameCtnBlockInfo::EWayPointType::StartFinish: - UI::Text("\\$ff6" + object.name); - if (UI::IsItemHovered()) infotext = "It's a multilap block/item"; - break; - default: - UI::Text(object.name); - break; - } - - UI::TableNextColumn(); - UI::Text(object.type); - UI::TableNextColumn(); - UI::Text(object.source); - UI::TableNextColumn(); - if (object.size == 0 && object.source != "In-Game" && object.source != "In TP") { - UI::Text("\\$555" + Text::Format("%lld",object.size)); - if (UI::IsItemHovered()) infotext = "Impossible to get the size of this block/item"; - } else { - if (object.icon) { - UI::Text("\\$fc0" + Text::Format("%lld",object.size)); - if (UI::IsItemHovered()) infotext = "All items with size in orange contains the icon. You must re-open the map to have the real size."; - } else { - UI::Text(Text::Format("%lld",object.size)); - } - } - - UI::TableNextColumn(); - UI::Text(Text::Format("%lld",object.count)); -} - -void Render() { - if (!menu_visibility) { - return; - } +void RenderInterface() { + if (!menu_visibility) return; + if (!RenderLib::InMapEditor()) 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 Blocks & Items Counter###Blocks & Items Counter", menu_visibility); - if (editor !is null) { + UI::PushStyleVar(UI::StyleVar::WindowMinSize, vec2(600, 400)); + if (UI::Begin("\\$cf9" + Icons::Table + "\\$z Blocks & Items Counter###Blocks & Items Counter", menu_visibility, UI::WindowFlags::NoCollapse)) { 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..."; + RenderLib::LoadingButton(); } else { - if (UI::Button(Icons::SyncAlt + " Refresh")) { + if (UI::Button(Icons::Refresh + " Refresh")) { refreshobject = true; forcesort = true; } } UI::SameLine(); + UI::PushStyleColor(UI::Col::FrameBg, vec4(0.169,0.388,0.651,0.1)); include_default_objects = UI::Checkbox("Include In-Game Blocks and Items", include_default_objects); + + UI::SameLine(); + UI::Dummy(vec2(UI::GetWindowSize().x - 600, 10)); + UI::SameLine(); + UI::SetNextItemWidth(200); + if (refreshobject) { + searchStr = ""; + string newSearchStr = UI::InputText("Filter", searchStr, UI::InputTextFlags(UI::InputTextFlags::AutoSelectAll | UI::InputTextFlags::NoUndoRedo | UI::InputTextFlags::ReadOnly)); + } else { + string newSearchStr = UI::InputText("Filter", searchStr, UI::InputTextFlags(UI::InputTextFlags::AutoSelectAll | UI::InputTextFlags::NoUndoRedo)); + if (newSearchStr != searchStr) { + searchStr = newSearchStr; + string searchStrLower = searchStr.ToLower(); + sortableobjects = {}; + for(uint i = 0; i < objects.Length; i++) { + if(searchStrLower == "" || objects[i].name.ToLower().Contains(searchStrLower)) { + sortableobjects.InsertLast(objects[i]); + } + } + } + } + UI::PopStyleColor(); + UI::Separator(); vec2 winsize = UI::GetWindowSize(); winsize.x = winsize.x-10; winsize.y = winsize.y-105; - if (UI::BeginTable("ItemsTable", 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); + if (UI::BeginTable("ItemsTable", 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("Item Name", UI::TableColumnFlags::None, 55.f, ESortColumn::ItemName); UI::TableSetupColumn("Type", UI::TableColumnFlags::None, 7.f, ESortColumn::Type); UI::TableSetupColumn("Source", UI::TableColumnFlags::None, 13.f, ESortColumn::Source); @@ -368,34 +136,29 @@ void Render() { } if (sortableobjects.Length > 0 ) { for(uint i = 0; i < sortableobjects.Length; i++) { - GenerateRow(sortableobjects[i]); + RenderLib::GenerateRow(sortableobjects[i]); } - } else { + } else if (refreshobject) { // Display the items during the refresh for(uint i = 0; i < objects.Length; i++) { - GenerateRow(objects[i]); + RenderLib::GenerateRow(objects[i]); } } UI::EndTable(); UI::Separator(); UI::Text(Icons::Info + " " + infotext); } - } else { - UI::Text("Open this plugin in the map editor"); } - UI::End(); + UI::End(); + UI::PopStyleVar(); } void RenderMenu() { - CGameCtnEditorFree@ editor = cast(GetApp().Editor); - CGameCtnChallenge@ map = cast(GetApp().RootMap); - - if (map is null && editor is null) { - return; - } + if (!RenderLib::InMapEditor()) return; if(UI::MenuItem("\\$cf9" + Icons::Table + "\\$z Blocks & Items Counter", "", menu_visibility)) { menu_visibility = !menu_visibility; refreshobject = true; + forcesort = true; } } diff --git a/BlocksItemsCounter/Source/Libs/Objects.as b/BlocksItemsCounter/Source/Libs/Objects.as new file mode 100644 index 0000000..e42b6a5 --- /dev/null +++ b/BlocksItemsCounter/Source/Libs/Objects.as @@ -0,0 +1,195 @@ +class Objects { //Items or Blocks + string name; + int trigger; // CGameItemModel::EnumWaypointType or CGameCtnBlockInfo::EWayPointType + string type; + string source; + int size; + int count; + bool icon; + array positions; + + Objects(const string &in name, int trigger, bool icon, const string &in type, const string &in source, int size, vec3 pos ) { + this.name = name; + this.trigger = trigger; + this.count = 1; + this.type = type; + this.icon = icon; + this.source = source; + this.size = size; + this.positions = {pos}; + } +} + +// Force to split the refresh functions to bypass the script execution delay on heavy maps +void RefreshBlocks() { + auto map = GetApp().RootMap; + + if (map !is null) { + // 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++) { + int idifexist = -1; + string blockname; + bool isofficial = true; + blockname = blocks[i].BlockModel.IdName; + if (blockname.ToLower().SubStr(blockname.Length - 22, 22) == ".block.gbx_customblock") { + isofficial = false; + blockname = blockname.SubStr(0, blockname.Length - 12); + } + + if (include_default_objects || blockname.ToLower().SubStr(blockname.Length - 10, 10) == ".block.gbx") { + vec3 pos; + if (blocks[i].CoordX != 4294967295 && blocks[i].CoordZ != 4294967295) { // Not placed in free mapping + if (pluginmaptype !is null) { // Editor plugin is available + pos = pluginmaptype.GetVec3FromCoord(blocks[i].Coord); + } else { + pos.x = blocks[i].CoordX * 32 + 16; + pos.y = (blocks[i].CoordY - 8) * 8 + 4; + pos.z = blocks[i].CoordZ * 32 + 16; + } + } else { + pos = Dev::GetOffsetVec3(blocks[i], 0x6c); + // center the coordinates in the middle of the block + pos.x += 16; + pos.y += 4; + pos.z += 16; + } + + int index = objectsindex.Find(blockname); + + if (index >= 0) { + objects[index].count++; + objects[index].positions.InsertLast(pos); + } else { + int trigger = blocks[i].BlockModel.EdWaypointType; + AddNewObject(blockname, trigger, "Block", pos, 0, isofficial); + objectsindex.InsertLast(blockname); + } + } + if (i % 100 == 0) yield(); // to avoid timeout + } + } +} + +// Force to split the refresh functions to bypass the script execution delay on heavy maps +void RefreshItems() { + auto map = GetApp().RootMap; + + if (map !is null) { + // Items + auto items = map.AnchoredObjects; + for(uint i = 0; i < items.Length; i++) { + int idifexist = -1; + string itemname = items[i].ItemModel.IdName; + int fallbacksize = 0; + bool isofficial = true; + + if (itemname.ToLower().SubStr(itemname.Length - 9, 9) == ".item.gbx") { + isofficial = false; + auto article = cast(items[i].ItemModel.ArticlePtr); + if (article !is null) { + itemname = string(article.PageName) + string(article.Name) + ".Item.Gbx"; + } else { + auto fid = cast(GetFidFromNod(items[i].ItemModel)); + fallbacksize = fid.ByteSize; + } + } + + if (include_default_objects || itemname.ToLower().SubStr(itemname.Length - 9, 9) == ".item.gbx") { + int index = objectsindex.Find(itemname); + if (index >= 0) { + objects[index].count++; + objects[index].positions.InsertLast(items[i].AbsolutePositionInMap); + } else { + int trigger = items[i].ItemModel.WaypointType; + AddNewObject(itemname, trigger, "Item", items[i].AbsolutePositionInMap, fallbacksize, isofficial); + objectsindex.InsertLast(itemname); + } + } + if (i % 100 == 0) yield(); // to avoid timeout + } + } +} + +void AddNewObject(const string &in objectname, int trigger, const string &in type, vec3 pos, int fallbacksize, bool isofficial) { + bool icon = false; + int size; + string source; + CSystemFidFile@ file; + CGameCtnCollector@ collector; + CSystemFidFile@ tempfile; + + if (type == "Item" && Regex::IsMatch(objectname, "^[0-9]*/.*.zip/.*", Regex::Flags::None)) {// ItemCollections + source = "Club"; + @file = Fids::GetFake('MemoryTemp\\FavoriteClubItems\\' + objectname); + @collector = cast(cast(file.Nod)); + if (collector is null || (collector.Icon !is null || file.ByteSize == 0)) { + @tempfile = Fids::GetFake('MemoryTemp\\CurrentMap_EmbeddedFiles\\ContentLoaded\\ClubItems\\' + objectname); + } + } else { // Blocks and Items + source = "Local"; + @file = Fids::GetUser(type + 's\\' + objectname); + @collector = cast(cast(file.Nod)); + if (collector is null || (collector.Icon !is null || file.ByteSize == 0)) { + @tempfile = Fids::GetFake('MemoryTemp\\CurrentMap_EmbeddedFiles\\ContentLoaded\\' + type + 's\\' + objectname); + + if (type == "Block" && tempfile.ByteSize == 0) { // Block is in Items dir + @tempfile = Fids::GetFake('MemoryTemp\\CurrentMap_EmbeddedFiles\\ContentLoaded\\Items\\' + objectname); + } + } + } + if (tempfile !is null) { + if (collector !is null && collector.Icon !is null && tempfile.ByteSize == 0) { + icon = true; + size = file.ByteSize; + } else { + size = tempfile.ByteSize; + } + if (isofficial) { + source = "In-Game"; + } else if (file.ByteSize == 0 && tempfile.ByteSize == 0 && fallbacksize == 0) { +#if TMNEXT + source = "Local"; +#else + source = "In TP"; +#endif + } else if (file.ByteSize == 0 && tempfile.ByteSize == 0 && fallbacksize > 0 ) { + source = "Embedded"; + size = fallbacksize; + } else if (file.ByteSize == 0 && tempfile.ByteSize > 0) { + source = "Embedded"; + } + } else { + size = file.ByteSize; + } + + objects.InsertLast(Objects(objectname, trigger, icon, type, source, size, pos)); +} + +bool FocusCam(const string &in objectname) { + auto editor = cast(GetApp().Editor); + auto camera = editor.OrbitalCameraControl; + auto map = GetApp().RootMap; + + + if (camera !is null) { + int index = objectsindex.Find(objectname); + + camerafocusindex++; + + if (camerafocusindex > objects[index].positions.Length - 1 ) { + camerafocusindex = 0; + } + + camera.m_TargetedPosition = objects[index].positions[camerafocusindex]; + // Workaround to update camera TargetedPosition + editor.ButtonZoomInOnClick(); + editor.ButtonZoomOutOnClick(); + return true; + } + return false; +} \ No newline at end of file diff --git a/BlocksItemsCounter/Source/Libs/Render.as b/BlocksItemsCounter/Source/Libs/Render.as new file mode 100644 index 0000000..1878034 --- /dev/null +++ b/BlocksItemsCounter/Source/Libs/Render.as @@ -0,0 +1,74 @@ +namespace RenderLib +{ + bool InMapEditor() { + CGameCtnEditorFree@ editor = cast(GetApp().Editor); + CGameCtnChallenge@ map = cast(GetApp().RootMap); + + if (map !is null && editor !is null) { + return true; + } + return false; + } + + void LoadingButton() { + 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..."; + } + + void GenerateRow(Objects@ object) { + UI::TableNextRow(); + UI::TableNextColumn(); + if (UI::Button(Icons::Search + "###" + object.name)) { + FocusCam(object.name); + } + if (UI::IsItemHovered() && object.type == "Block" && 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(); + switch(object.trigger){ + case CGameCtnBlockInfo::EWayPointType::Start: + UI::Text("\\$9f9" + object.name); + if (UI::IsItemHovered()) infotext = "It's a start block/item"; + break; + case CGameCtnBlockInfo::EWayPointType::Finish: + UI::Text("\\$f66" + object.name); + if (UI::IsItemHovered()) infotext = "It's a finish block/item"; + break; + case CGameCtnBlockInfo::EWayPointType::Checkpoint: + UI::Text("\\$99f" + object.name); + if (UI::IsItemHovered()) infotext = "It's a checkpoint block/item"; + break; + case CGameCtnBlockInfo::EWayPointType::StartFinish: + UI::Text("\\$ff6" + object.name); + if (UI::IsItemHovered()) infotext = "It's a multilap block/item"; + break; + default: + UI::Text(object.name); + break; + } + + UI::TableNextColumn(); + UI::Text(object.type); + UI::TableNextColumn(); + UI::Text(object.source); + UI::TableNextColumn(); + if (object.size == 0 && object.source != "In-Game" && object.source != "In TP") { + UI::Text("\\$555" + Text::Format("%lld",object.size)); + if (UI::IsItemHovered()) infotext = "Impossible to get the size of this block/item"; + } else { + if (object.icon) { + UI::Text("\\$fc0" + Text::Format("%lld",object.size)); + if (UI::IsItemHovered()) infotext = "All items with size in orange contains the icon. You must re-open the map to have the real size."; + } else { + UI::Text(Text::Format("%lld",object.size)); + } + } + + UI::TableNextColumn(); + UI::Text(Text::Format("%lld",object.count)); + } +} diff --git a/BlocksItemsCounter/info.toml b/BlocksItemsCounter/info.toml index cb22e3f..2e96e29 100644 --- a/BlocksItemsCounter/info.toml +++ b/BlocksItemsCounter/info.toml @@ -3,5 +3,5 @@ name = "Blocks & Items Counter" author = "Beu" category = "Map Editor" siteid = 97 -version = "1.3" +version = "1.4" blocks = [ "Plugin_Blocks&ItemsCounter" ]