diff --git a/cubiomes-viewer.pro b/cubiomes-viewer.pro index d7a4aee..14ad13a 100644 --- a/cubiomes-viewer.pro +++ b/cubiomes-viewer.pro @@ -130,6 +130,7 @@ SOURCES += \ src/tabbiomes.cpp \ src/tablocations.cpp \ src/tabstructures.cpp \ + src/tabslime.cpp \ src/mainwindow.cpp \ src/main.cpp \ src/util.cpp \ @@ -197,6 +198,7 @@ HEADERS += \ src/tabbiomes.h \ src/tablocations.h \ src/tabstructures.h \ + src/tabslime.h \ src/mainwindow.h \ src/util.h \ src/widgets.h \ @@ -220,7 +222,8 @@ FORMS += \ src/rangedialog.ui \ src/tabbiomes.ui \ src/tablocations.ui \ - src/tabstructures.ui + src/tabstructures.ui \ + src/tabslime.ui RESOURCES += \ rc/icons.qrc \ diff --git a/rc/lang/zh_CN.qm b/rc/lang/zh_CN.qm index 06a01c4..74e94af 100644 Binary files a/rc/lang/zh_CN.qm and b/rc/lang/zh_CN.qm differ diff --git a/rc/lang/zh_CN.ts b/rc/lang/zh_CN.ts index e2d7e9f..df7060f 100644 --- a/rc/lang/zh_CN.ts +++ b/rc/lang/zh_CN.ts @@ -693,34 +693,34 @@ MC版本: 1.X - + Select category 选择目录 - + Algorithm helpers 算法辅助 - + Quad-structure 四联结构 - + Structures 普通结构 - + Biomes 群系 - + Select type 选择种类 @@ -746,7 +746,7 @@ - + Location 范围 @@ -755,7 +755,7 @@ - + Lower bound (inclusive) 最小值(含) @@ -787,7 +787,7 @@ - + Upper bound (inclusive) 最大值(含) @@ -1300,8 +1300,8 @@ yield each sampled position individually 包含地下室 - - + + Generates any of: 生成以下任意一个: @@ -1311,121 +1311,121 @@ yield each sampled position individually 条件描述 - + MC %1 Minecraft version MC版本 MC版本 %1 - + Other 其他 - + Oceanic 海洋类 - + Warm 温带类 - + Lush 繁茂类 - + Cold 寒冷类 - + Freezing 冰冻类 - + Special Warm 特殊温带类 - + Special Lush 特殊繁茂类 - + Special Cold 特殊寒冷类 - + Temperature 温度 - + Humidity 湿度 - + Continentalness 海陆分布 - + Erosion 侵蚀度 - + Depth 高度/深度 - + Weirdness 稀有程度 + - -Inf 无下限 + - +Inf 无上限 - + Require full range instead of intersection 全范围而非交叉 - + -Inf 无下限 - + +Inf 无上限 - + [script not found] [未找到脚本] - + The biome locator checks for %n instance(s), each of size %1, which cannot be satisfied by an area of size %2%3%4 = %5 < %6 @ scale 1:%7. @@ -1433,149 +1433,149 @@ yield each sampled position individually - + Cave biomes do not generate above Y = 246. The sampling height should be lowered. 洞穴群系无法生成在 Y >= 246 的地方, 请考虑降低采样高度。 - - + + Continue anyway? 是否继续? - + The selected area does not contain a range where a quad-structure can generate. 这片区域无法生成任何四联结构。 - + <html><head/><body><p>The area can be entered via <b>custom</b> rectangle, that is defined by its two opposing corners, relative to a center point. These bounds are inclusive.</p><p>Alternatively, the area can be defined as a <b>centered square</b> with a certain side length. In this case the area has the bounds: [-X/2, +X/2] on both axes, rounding down and bounds included. For example a centered square with side 3 will go from -2 to 1 for both the X and Z axes.</p><p>Some filters have a scaling associated with them. This means the condition only checks on a grid with that spacing. An area with a range from -21 to 21 at scale 1:16 may effectively be expanded to -32 to 31, and get sampled at -32, -16, 0 and 16.</p></body></html> - + From floor(-x/2) to floor(x/2) on both axes (inclusive) X、Z坐标从 floor(-x/2) (含)到 floor(x/2) (含) - + Sampling scale: 取样密度: - + Generation layer: 生成精度: - + No allowed start pieces specified. Condition can never be true. 该结构不会生成该子类别, 该条件将无法满足! - + Missing Start Piece 错误的子类别 - + The condition contains a climate range which is unbounded with the full range required, which can never be satisfied. 该条件包含了越界的气候条件范围, 该条件将无法满足! - + Bad Climate Range 错误的气候条件范围 - + Area Insufficient 区域不够大 - + Bad Surface Height 采样高度过高 - + No Allowed Biomes 未选择允许生成的群系 - + The set of allowed biomes is empty, which can never be satisfied. Please include some biomes for the required proportion. 未选择允许生成的群系,条件无法满足。请增加一些允许生成的群系! - + Bad Area for Quad-Structure 无法生成四联结构 - + Help: area entry - + (~%1 sq. chunks) (~%1 平方区块) - + (%1 sq. chunks) (%1 平方区块) - + Unsaved changes 更改未保存 - + Discard unsaved changes? 确认丢弃所有未保存的更改? - + Save lua script 保存Lua脚本 - + Lua script (*.lua) Lua脚本 (*.lua) - + Empty check functions 检查函数为空 - + Village along the way from A to B 从A到B沿路上的村庄 - + Lua examples Lua 脚本示例 - + Replace editor content with example: 将编辑器内容替换为示例 - + Help: Lua script - + <html><head/><body><p>Lua scripts allow the user to write custom filters. A valid Lua filtering script has to define a</p><p><b>check(seed, at, deps)</b></p><p>function, that evaluates when a seed satisfies the condition. It should return a <b>x, z</b> value pair that is the block position for other conditions to reference as the relative location. If the condition fails, the function can return <b>nil</b> instead.</p><p>The arguments of <b>check()</b> are in order:</p><p><dl><dt><b>seed</b><dd>the current world seed<dt><b>at</b> = {x, z}<dd>the relative location of the parent condition<dt><b>deps</b> = [..]{x, z, id, parent}<dd>a list of tables with information on the dependent conditions (i.e. those later in the conditions list)</dl></p><p>Optionally, the script can also define a <b>check48()</b> function, with a similar prototype, that tests whether a given 48-bit seed base is worth investigating further.</p><p>A few global symbols are predefined. These include the biome ID and structure type enums from cubiomes, which means they can be referred to by their names (such as <b>flower_forest</b> or <b>Village</b>). Furthermore, the following functions are available:</p><p><dl><dt><b>getBiomeAt(x, z)</b><dt><b>getBiomeAt(x, y, z)</b><dd>returns the overworld biome at the given block coordinates</p><p><dt><b>getStructures(type, x1, z1, x2, z2)</b><dd>returns a list of <b>{x, z}</b> structure positions for the specified structure <b>type</b> within the area spanning the block positions <b>x1, z1</b> to <b>x2, z2</b>, or <b>nil</b> upon failure</p></body></html> @@ -2194,429 +2194,441 @@ Leave blank for the default behaviour - + OR logic gate 逻辑或 - + Evaluates as true when any of the conditions that reference it (by relative location) are met. When no referencing conditions are defined, it defaults to true. 符合其中任一条件即返回true, 没有条件时默认为true - + NOT logic gate 逻辑否 - + Evaluates as true when none of the conditions that reference it (by relative location) are met. When no referencing conditions are defined, it defaults to true. 没有任何符合的条件才返回true, 没有条件时默认为true - + Lua Lua - + Define custom conditions using Lua scripts. 使用Lua脚本自定义条件 - + Coordinate factor x/8 坐标系数 X/8 - + Divides relative location by 8, from Overworld to Nether. 将坐标除以8, 用作主世界到地狱的坐标转换 - + Coordinate factor x*8 坐标系数 X*8 - + Multiplies relative location by 8, from Nether to Overworld. 将坐标乘以8, 用作地狱到主世界的坐标转换 - + Spiral iterator 螺旋迭代器 - + <html><head/><body>Spiral iterator conditions can be used to move a testing position across a given area using a certain step size. Other conditions that refer to it as a relative location will be checked at each step. The iteration is performed in a spiral, so positions closer to the center get priority.</body></html> 螺旋迭代器可以让其连带的搜索条件以固定步长遍历整片区域, 每一步所有条件都会重新检查一遍。 迭代器会以螺旋状路径状遍历整片区域, 所以靠近区域中心的地方会被优先遍历到。 - + Quad-hut (ideal) 四联女巫小屋(理想型) - + The lower 48-bits provide potential for four swamp huts in spawning range, in one of the best configurations that exist. 种子的低48位(二进制)决定了该种子具有成为最佳配置的四联女巫小屋的可能性 - + Quad-hut (classic) 四联女巫小屋(经典型) - + The lower 48-bits provide potential for four swamp huts in spawning range, in one of the "classic" configurations. (Checks for huts in the nearest 2x2 chunk corners of each region.) 种子的低48位(二进制)决定了该种子具有成为经典配置的四联女巫小屋的可能性 - + Quad-hut (normal) 四联女巫小屋(普通型) - + The lower 48-bits provide potential for four swamp huts in spawning range, such that all of them are within 128 blocks of a single AFK location, including a vertical tolerance for a fall damage chute. 种子的低48位(二进制)决定了该种子具有成为普通配置的四联女巫小屋的可能性 (保证四个小屋都在单人挂机距离内并且都有足够的垂直空间来摔死女巫) - + Quad-hut (barely) 四联女巫小屋(勉强型) - + The lower 48-bits provide potential for four swamp huts in spawning range, in any configuration, such that the bounding boxes are within 128 blocks of a single AFK location. 种子的低48位(二进制)决定了该种子具有成为最差配置的四联女巫小屋的可能性 (只能保证四个小屋都在单人挂机距离内) - + Quad-ocean-monument (>95%) 四联海底神殿(>95%) - + The lower 48-bits provide potential for 95% of the area of four ocean monuments to be within 128 blocks of an AFK location. 种子的低48位(二进制)决定了该种子具有成为四联海底神殿的可能性, 并且有超过95%的面积落在单人挂机距离内 - + Quad-ocean-monument (>90%) 四联海底神殿(>90%) - + The lower 48-bits provide potential for 90% of the area of four ocean monuments to be within 128 blocks of an AFK location. 种子的低48位(二进制)决定了该种子具有成为四联海底神殿的可能性, 并且有超过90%的面积落在单人挂机距离内 - + Allows only seeds with the included (+) biomes in the specified area and discard those that have biomes that are explicitly excluded (-). 在指定范围内包括所有你想要的(+)群系并排除所有你不要的(-) - + Allows only seeds with the included (+) biomes in the specified area and discard those that have biomes that are explicitly excluded (-) at layer RIVER with scale 1:4. This layer does not generate ocean variants. 在指定范围内包括所有你想要的(+)群系并排除所有你不要的(-) 但是只生成到1:4层的河流为止, 不生成海洋变种 - + Allows only seeds with the included (+) biomes in the specified area and discard those that have biomes that are explicitly excluded (-) at layer OCEAN TEMPERATURE with scale 1:256. This generation layer depends only on the lower 48-bits of the seed. 在指定范围内包括所有你想要的(+)群系并排除所有你不要的(-) 仅生成到决定海洋温度的1:256 这部分群系生成仅由种子低48位决定 - + Custom limits for the required and allowed climate noise parameters that the specified area should cover. 自定义指定区域内的群系的气候参数限制 - + Locate climate minimum/maximum 定位气候参数的极值 - - + + Climate noise samples 气候参数噪声采样 - + Samples climate noise in a given area to find if a proportion of the biomes match a set of allowed biomes. 判断指定区域内的气候参数是否有指定的比例满足要求 - + Finds the location where a climate parameter reaches its minimum or maximum. 找到该区域中气候参数的极值 - + Finds the center position of a given biome. 找到给定群系的中心点 - + Locate biome center 1:256 群系中心定位器 1:256 - + Finds the center position of a given biome. Based on the 1:256 biome layer. 基于1:256群系层找到给定群系的中心点 - + Temperature categories 温度类别 - + Checks that the area has a minimum of all the required temperature categories. 检查这块区域是否包含大于等于你所指定的数目的温度群系 - + Ocean ruin 海底遗迹 - + Biome samples 群系比例 + Double Ocean Monument + 二联海底神殿 + + + + Requires two ocean monuments where one monument is within 180 blocks of the other (forming a circle centered at one monument). At least one monument must be within the specified search range. The x and z differences between the two monuments must not be 160 and 80 respectively (i.e., this is the only invalid configuration but still valid for the filter). + 简而言之:在给定的范围内存在可以同时被单人刷怪范围完全覆盖的两个海底神殿 + 要求以其中一个海底神殿为圆心半径180内存在另一个海底神殿的中心(且其xz差值不为160,80或80,160) +任意一个海底神殿的中心位于给定范围内条件即可成立 + + + Samples biomes in a given area to find if a proportion of the biomes match a set of allowed biomes. 判断指定区域内的群系比例是否满足指定的群系比例 - + Overworld at scale 主世界群系 - + Nether at scale 下界群系 - + Nether biomes sampled on a scaled grid. 以一定比例取样的下界群系 - + End at scale 末地群系 - + End biomes sampled on a scaled grid. 以一定比例取样的末地群系 - + Biome layer 1:4 RIVER 群系筛选 1:4 河流 - + Biome layer 1:256 O.TEMP 群系筛选 1:256 海洋温度 - + Climate parameters 群系气候参数 - + Locate climate extreme 定位群系参数极值 - + Locate biome center 定位群系中心 - + Spawn 出生点 - + Slime chunk 史莱姆区块 - + Surface height 地表高度 - + Check the approximate surface height at scale 1:4 at a single coordinate. 以1:4的比例在某个坐标检查大致地表高度 - + First stronghold 首个要塞 - + Finds the approxmiate location of the first stronghold (+/-112 blocks). Depends only on the 48-bit seed. 仅依靠低48位(二进制)找到第一个要塞的大致位置(+/-112格) - + Stronghold 要塞 - + Village 村庄 - + Abandoned mineshaft 废弃矿井 - + Desert pyramid 沙漠神殿 - + In version 1.18+, desert pyramids depend on surface height and may fail to generate near caves/aquifers, rivers and oceans. 注意, 在1.18中, 林地府邸、沙漠神殿和丛林神殿的生成还会考虑其表面的高度, 所以其在(含水)洞穴、河流或者海洋群系(甚至是较高的沙丘)周围可能会生成失败 - + Jungle temple 丛林神殿 - + In version 1.18+, jungle temples depend on surface height and may fail to generate near caves/aquifers, rivers and oceans. 注意, 在1.18中, 林地府邸、沙漠神殿和丛林神殿的生成还会考虑其表面的高度, 所以其在(含水)洞穴、河流或者海洋群系(甚至是较高的沙丘)周围可能会生成失败 - + Swamp hut 女巫小屋 - + Ocean monument 海底神殿 - + Igloo 冰屋 - + Woodland mansion 林地府邸 - + In version 1.18+, mansions depend on surface height and may fail to generate near caves/aquifers, rivers and oceans. 注意, 在1.18中, 林地府邸、沙漠神殿和丛林神殿的生成还会考虑其表面的高度, 所以其在(含水)洞穴、河流或者海洋群系(甚至是较高的沙丘)周围可能会生成失败 - + Shipwreck 沉船 - + Buried treasure 宝藏 - + Buried treasures are always positioned near the center of a chunk rather than a chunk boarder. Make sure the testing area is set accordingly. 宝藏总是出现在区块中心附近而不是区块边界附近, 请合理划定查找范围 - + Desert well 沙漠水井 - + Pillager outpost 掠夺者前哨站 - + Ancient city 远古城市 - + Trail ruins 古迹废墟 - + Trial chambers 试炼密室 - + Ruined portal (overworld) 废弃传送门(主世界) - + Ruined portal (nether) 废弃传送门(下界) - + Nether fortress 下界要塞 - + Bastion remnant 堡垒遗迹 - + End city 末地城 - + End gateway 末地(返程)折跃门 - + Checks only scattered return gateways. Does not include those generated when defeating the dragon. 特指返程折跃门, 而非那些你打龙开的折跃门 - + function check() was not defined 未定义check()函数! @@ -3153,42 +3165,52 @@ Applies only to feature-structures of region-size = 32 and chunk-gap = 8, in par GotoDialog - + Coordinates 坐标 - + Coordinates: 坐标: - + + coord text: + 坐标文本: + + + + Interpret + 转换坐标 + + + X X - + Z Z - + Scale: 缩放等级: - + A very large scale may be unsafe 缩放比例过大可能导致卡死 - + blocks per pixel 方块/像素 - + Animate travel 播放移动动画 @@ -3196,52 +3218,52 @@ Applies only to feature-structures of region-size = 32 and chunk-gap = 8, in par Layer - + 1:1 Voronoi 1:1 泰森多边形 - + 1:4 River Mix 1:4 河流 - + 1:4 Ocean Mix 1:4 海洋 - + 1:4 Zoom 1:4 缩放 - + 1:16 Swamp River 1:16 沼泽河流 - + 1:16 Shore 1:16 海岸 - + 1:64 Hills 1:64 丘陵 - + 1:64 Sunflower 1:64 向日葵平原 - + 1:256 Biome 1:256 群系 - + 1:256 Bamboo 1:256 竹林 @@ -3497,7 +3519,7 @@ Applies only to feature-structures of region-size = 32 and chunk-gap = 8, in par - + Map 地图 @@ -3758,8 +3780,8 @@ Applies only to feature-structures of region-size = 32 and chunk-gap = 8, in par - - + + Undock map 开启地图小窗 @@ -3794,154 +3816,159 @@ Applies only to feature-structures of region-size = 32 and chunk-gap = 8, in par (Beta 1.7) 湿度分布图 - + Biomes 群系 - + Structures 结构 - + Zoom In 放大 - + Zoom Out 缩小 - + Overworld 主世界 - + Nether 下界 - + End 末地 - + Show %1 显示 %1 - + Conditions 条件 - + Locations 结构坐标 - + + Slime + 史莱姆区块 + + + Go to Origin 前往坐标原点并重置缩放比例 - + Help: Conditions - + <html><head/><body><p>The search conditions define the properties by which potential seeds are filtered.</p><p>Conditions can reference each other to produce relative positional dependencies (indicated with the ID in square brackets [XY]). When a condition passes its check, it usually yields just one location that other conditions can reference. An exception to this are structure conditions with exactly one required instance. In this case, each found structure occurence is examined separately instead. On the other hand, a condition that checks for a structure cluster, will average the position of all occurences and yield a single position.</p><p>Standard biome conditions yield the center of the testing area as they evaluate the area as a whole. To locate the position of a given biome you can use the designated <b>locate</b> filters, or use a spiral iterator to scan an area with a localized condition.</p></body></html> - + Seed generator (48-bit) 种子生成器(低48二进制位) - + Help: Seed generator - + <html><head/><body><p>For some searches, the 48-bit structure seed candidates can be generated without searching, which can vastly reduce the search space that has to be checked.</p><p>The generator mode <b>Auto</b> is recommended for general use, which automatically selects suitable options based on the conditions list.</p><p>The <b>Quad-feature</b> mode produces candidates for quad&#8209;structures that have a uniform distribution of region&#8209;size=32 and chunk&#8209;gap=8, such as swamp huts.</p><p>A perfect <b>Quad-monument</b> structure constellation does not actually exist, but some extremely rare structure seed bases get close, with over 90&#37; of the area within 128 blocks. The generator uses a precomputed list of these seed bases.</p><p>Using a <b>Seed list</b> you can provide a custom set of 48-bit candidates. Optionally, a salt value can be added and the seeds can be region transposed.</p></body></html> - + Matching seeds 符合条件的种子 - + Help: Matching seeds - + <html><head/><body><p>The list of seeds acts as a buffer onto which suitable seeds are added when they are found. You can also copy the seed list, or paste seeds into the list. Selecting a seed will open it in the map view.</p></body></html> - + Failed to open file: "%1" 无法打开以下文件: "%1" - + text Seed input type 种子导入文件类型 文本文件 - + random Seed input type 种子导入文件类型 任意文件 - + Save progress 导出进度 - - + + Session files (*.session *.txt);;Any files (*) 会话文件 (*.session *.txt);;任意文件 (*) - + Load progress 导入进度 - + Save screenshot 保存截图 - + Images (*.png *.jpg *.ppm) 图像文件 (*.png *.jpg *.ppm) - + Redock map 关闭地图小窗 - + The application will need to be restarted before all changes can take effect. Cubiomes Viewer需要重启以应用所有更改 @@ -4298,7 +4325,7 @@ In versions 1.19 - 1.19.2, the world generation can have interesting artifacts a 信息 - + Failed to open session file: "%1" 无法打开以下进度文件: @@ -4989,6 +5016,184 @@ condition missing or out of order. "%1" + + TabSlime + + + Form + 史莱姆区块 + + + + + Calculate optimal AFK positions for slime chunk coverage (24-128 blocks radius). + 计算最佳史莱姆农场挂机位置 + + + + + Seed(s): + 种子: + + + + + Current seed + 当前种子 + + + + + From matching seeds list + 符合条件的种子列表 + + + + + + + Lower bound (inclusive) + 最小值(含) + + + + <html><head/><body><p>X<span style=" vertical-align:sub;">1</span>:</p></body></html> + X<sub>1</sub>: + + + + + + + 0 + 0 + + + + <html><head/><body><p>Z<span style=" vertical-align:sub;">1</span>:</p></body></html> + Z<sub>1</sub>: + + + + + + + Upper bound (inclusive) + 最大值(含) + + + + <html><head/><body><p>X<span style=" vertical-align:sub;">2</span>:</p></body></html> + X<sub>2</sub>: + + + + <html><head/><body><p>Z<span style=" vertical-align:sub;">2</span>:</p></body></html> + Z<sub>2</sub>: + + + + + From visible + 使用可见范围 + + + + + Minimum area: + 最小面积: + + + + 10000 + 10000 + + + + + Include biome parameters + 增加群系修正 + + + + <html><head/></head><body><p>Sample biomes at y=-64 (for MC 1.18+). Exclude mushroom fields and deep dark. Apply multipliers for dripstone caves/rivers (5/6) and old growth pine taiga (0.95).</p></body></html> + 在y=-64处取值(1.18+),排除深暗之域和蘑菇岛的史莱姆刷怪面积,河流和溶洞乘以5/6,原始云杉树林乘以0.95 + + + + + + seed + 种子 + + + + + + area + 区域 + + + + + + x + X + + + + + + z + Z + + + + + Export... + 导出... + + + + <html><head/><head/><body><p>Sample biomes at y=-64 (for MC 1.18+). Exclude mushroom fields and deep dark. Apply multipliers for dripstone caves/rivers (5/6) and old growth pine taiga (0.95).</p></body></html> + 在y=-64处取值(1.18+),排除深暗之域和蘑菇岛的史莱姆刷怪面积,河流和溶洞乘以5/6,原始云杉树林乘以0.95 + + + + No seeds to process. + 无待搜索的种子 + + + + + Stop + 停止 + + + + + + Analyze + 统计 + + + + Export slime results + 导出史莱姆区块统计结果 + + + + Text files (*.txt *.csv);;Any files (*) + 文本文件 (*.txt *csv);;任意文件 (*) + + + + Failed to open file for export: +"%1" + 无法打开以下导出文件: +"%1" + + TabStructures @@ -5023,7 +5228,7 @@ condition missing or out of order. - + Analyze 统计 @@ -5050,15 +5255,17 @@ condition missing or out of order. - - - + + + + + seed 种子 - + structure 结构 @@ -5070,22 +5277,26 @@ condition missing or out of order. - - + + + + x X - - + + + + z Z - + details 详细信息 @@ -5096,95 +5307,116 @@ condition missing or out of order. - + + + type 种类 - + + + distance 距离 - + radius 半径 - + spawn area 有效刷怪面积 - - - - + + Double-Structures + 二联结构 + + + + + dx + dx + + + + + dz + dz + + + + + + Lower bound (inclusive) 最小值(含) - + <html><head/><body><p>X<span style=" vertical-align:sub;">1</span>:</p></body></html> X<sub>1</sub>: - - - - + + + + 0 0 - + <html><head/><body><p>Z<span style=" vertical-align:sub;">1</span>:</p></body></html> Z<sub>1</sub>: - - - - + + + + Upper bound (inclusive) 最大值(含) - + <html><head/><body><p>X<span style=" vertical-align:sub;">2</span>:</p></body></html> X<sub>2</sub>: - + <html><head/><body><p>Z<span style=" vertical-align:sub;">2</span>:</p></body></html> Z<sub>2</sub>: - + From visible 使用可见范围 - - + + Stop 停止 - + Export structure analysis 导出结构统计结果 - + Text files (*.txt *csv);;Any files (*) 文本文件 (*.txt *csv);;任意文件 (*) - + Failed to open file for export: "%1" 无法打开以下导出文件: diff --git a/src/gotodialog.cpp b/src/gotodialog.cpp index a128229..652755e 100644 --- a/src/gotodialog.cpp +++ b/src/gotodialog.cpp @@ -6,6 +6,7 @@ #include #include #include +#include static bool g_animate; @@ -34,6 +35,59 @@ GotoDialog::~GotoDialog() delete ui; } +bool GotoDialog::parseCoordinates(const QString &input, qreal &x, qreal &z) +{ + QString text = input.trimmed(); + if (text.isEmpty()) + return false; + + // 移除 /tp 命令前缀(如果存在) + if (text.startsWith("/tp", Qt::CaseInsensitive)) + { + text = text.mid(3).trimmed(); + } + + // 使用正则表达式提取所有数字 + QRegularExpression numberRegex("-?\\d+\\.?\\d*"); + QRegularExpressionMatchIterator matches = numberRegex.globalMatch(text); + + QList numbers; + while (matches.hasNext()) + { + QRegularExpressionMatch match = matches.next(); + bool ok; + qreal num = match.captured().toDouble(&ok); + if (ok) + { + numbers.append(num); + } + } + + // 需要至少2个数字(x和z) + if (numbers.size() < 2) + return false; + + // 第一个数字是x,最后一个数字是z(忽略中间的y坐标) + x = numbers.first(); + z = numbers.last(); + + return true; +} + +void GotoDialog::on_buttonInterpret_clicked() +{ + QString coordInput = ui->lineCoordInput->text().trimmed(); + if (!coordInput.isEmpty()) + { + qreal x, z; + if (parseCoordinates(coordInput, x, z)) + { + ui->lineX->setText(QString::asprintf("%.1f", x)); + ui->lineZ->setText(QString::asprintf("%.1f", z)); + } + } +} + void GotoDialog::on_buttonBox_clicked(QAbstractButton *button) { QDialogButtonBox::StandardButton b = ui->buttonBox->standardButton(button); @@ -57,6 +111,7 @@ void GotoDialog::on_buttonBox_clicked(QAbstractButton *button) ui->lineX->setText("0"); ui->lineZ->setText("0"); ui->lineScale->setText("16"); + ui->lineCoordInput->clear(); } } diff --git a/src/gotodialog.h b/src/gotodialog.h index 7a40d5f..5d9b6df 100644 --- a/src/gotodialog.h +++ b/src/gotodialog.h @@ -22,6 +22,10 @@ class GotoDialog : public QDialog private slots: void on_lineScale_textChanged(const QString &text); void on_buttonBox_clicked(QAbstractButton *button); + void on_buttonInterpret_clicked(); + +private: + bool parseCoordinates(const QString &input, qreal &x, qreal &z); private: Ui::GotoDialog *ui; diff --git a/src/gotodialog.ui b/src/gotodialog.ui index a4f3275..7ae771d 100644 --- a/src/gotodialog.ui +++ b/src/gotodialog.ui @@ -2,6 +2,14 @@ GotoDialog + + + 0 + 0 + 344 + 149 + + Qt::StrongFocus @@ -20,6 +28,31 @@ + + + + + + coord text: + + + + + + + Qt::StrongFocus + + + + + + + Interpret + + + + + diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 68e7a3f..71d1b6f 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -12,6 +12,7 @@ #include "presetdialog.h" #include "tabbiomes.h" #include "tablocations.h" +#include "tabslime.h" #include "tabstructures.h" #include "util.h" #include "world.h" @@ -33,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -97,6 +99,7 @@ MainWindow::MainWindow(QString sessionpath, QString resultspath, QWidget *parent ui->tabContainerSearch->addTab(new TabLocations(this), tr("Locations")); ui->tabContainer->addTab(new TabBiomes(this), tr("Biomes")); ui->tabContainer->addTab(new TabStructures(this), tr("Structures")); + ui->tabContainer->addTab(new TabSlime(this), tr("Slime")); laction.resize(LOPT_MAX); laction[LOPT_BIOMES] = ui->actionBiomes; @@ -891,14 +894,42 @@ void MainWindow::on_actionRedistribute_triggered() std::vector conds = formCond->getConditions(); if (!conds.empty()) { + // Collect save IDs of quad conditions (main conditions) + QSet quadMainIds; + for (const Condition& c : conds) + { + const FilterInfo& ft = g_filterinfo.list[c.type]; + if (ft.cat == CAT_QUAD) + quadMainIds.insert(c.save); + } + + // Filter out auto-generated second conditions for quad conditions + // These are F_HUT or F_MONUMENT with count=4, rmax=256, and relative pointing to a quad main condition + std::vector filteredConds; + for (const Condition& c : conds) + { + bool isAutoGenerated = false; + if ((c.type == F_HUT || c.type == F_MONUMENT) && c.count == 4 && c.rmax == 256) + { + if (c.relative > 0 && quadMainIds.contains(c.relative)) + { + isAutoGenerated = true; + } + } + if (!isAutoGenerated) + { + filteredConds.push_back(c); + } + } + QMap ids; - for (int i = 0, n = conds.size(); i < n; i++) - ids.insert(conds[i].save, ids.size()+1); - for (int i = 0, n = conds.size(); i < n; i++) - if (conds[i].relative && !ids.contains(conds[i].relative)) - ids.insert(conds[i].relative, ids.size()+1); + for (int i = 0, n = filteredConds.size(); i < n; i++) + ids.insert(filteredConds[i].save, ids.size()+1); + for (int i = 0, n = filteredConds.size(); i < n; i++) + if (filteredConds[i].relative && !ids.contains(filteredConds[i].relative)) + ids.insert(filteredConds[i].relative, ids.size()+1); formCond->on_buttonRemoveAll_clicked(); - for (Condition& c : conds) + for (Condition& c : filteredConds) { c.save = ids[c.save]; c.relative = ids[c.relative]; @@ -1091,6 +1122,20 @@ void MainWindow::onActionMapToggled(int sopt, bool show) if (sopt == D_PORTAL) // overworld portals should also control nether getMapView()->setShow(D_PORTALN, show); getMapView()->setShow(sopt, show); + + // 当关闭史莱姆区块显示时,清除TabSlime中绘制的红圈 + if (sopt == D_SLIME && !show) + { + for (int i = 0; i < ui->tabContainer->count(); i++) + { + TabSlime *tabSlime = qobject_cast(ui->tabContainer->widget(i)); + if (tabSlime) + { + tabSlime->clearSlimeShapes(); + break; + } + } + } } void MainWindow::onActionBiomeLayerSelect(int mode, int disp) diff --git a/src/search.cpp b/src/search.cpp index 3ab75f1..dfac86b 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -1258,6 +1258,198 @@ testCondAt( return COND_OK; return COND_FAILED; + case F_DM: // Double Monument + { + // Find all monuments in the search area + rx1 = x1 >> 9; + rz1 = z1 >> 9; + rx2 = x2 >> 9; + rz2 = z2 >> 9; + + Pos monuments[MAX_INSTANCES]; + int mcnt = 0; + + // Collect all monument positions in the area + for (rz = rz1; rz <= rz2 && !*env->stop && mcnt < MAX_INSTANCES; rz++) + { + for (rx = rx1; rx <= rx2; rx++) + { + if (!getStructurePos(Monument, env->mc, env->seed, rx, rz, &pc)) + continue; + + // Check if monument is within search area + if (rmax) + { + int dx = pc.x - at.x; + int dz = pc.z - at.z; + int64_t rsq = dx*(int64_t)dx + dz*(int64_t)dz; + if (rsq >= rmax) + continue; + } + else if (pc.x < x1 || pc.x > x2 || pc.z < z1 || pc.z > z2) + { + continue; + } + + // Check viability if needed + if ((env->searchpass == PASS_FULL_64) || + (env->searchpass == PASS_FULL_48 && !finfo.dep64)) + { + if (*env->stop) return COND_FAILED; + env->init4Dim(finfo.dim); + int id = isViableStructurePos(Monument, &env->g, pc.x, pc.z, 0); + if (!id) + continue; + } + + monuments[mcnt++] = pc; + } + } + + // Need at least 2 monuments to form a pair + if (mcnt < 2) + { + // In fast pass, if we can't find enough monuments, fail immediately + if (env->searchpass == PASS_FAST_48) + return COND_FAILED; + // In full passes, we need to check viability first + if (env->searchpass == PASS_FULL_64) + return COND_FAILED; + if (env->searchpass == PASS_FULL_48 && !finfo.dep64) + return COND_FAILED; + return COND_MAYBE_POS_INVAL; + } + + // Check all pairs of monuments + const int64_t max_radius_sq = 180LL * 180LL; + bool found = false; + Pos best_center = {0, 0}; + + for (int i = 0; i < mcnt && !found; i++) + { + for (int j = i + 1; j < mcnt; j++) + { + Pos p1 = monuments[i]; + Pos p2 = monuments[j]; + + // Check if x and z differences are NOT 160 and 80 (quad configuration) + int dx = p2.x - p1.x; + int dz = p2.z - p1.z; + if (dx < 0) dx = -dx; + if (dz < 0) dz = -dz; + + // Skip if this is a standard quad configuration (160, 80) + if ((dx == 160 && dz == 80) || (dx == 80 && dz == 160)) + continue; + + // Check if one monument is within 180 blocks of the other + // Check both directions: p2 within circle centered at p1, or p1 within circle centered at p2 + int64_t dx12 = p2.x - p1.x; + int64_t dz12 = p2.z - p1.z; + int64_t dist12_sq = dx12*dx12 + dz12*dz12; + + int64_t dx21 = p1.x - p2.x; + int64_t dz21 = p1.z - p2.z; + int64_t dist21_sq = dx21*dx21 + dz21*dz21; + + // Check if either monument is within 180 blocks of the other + bool p2_in_circle_of_p1 = (dist12_sq < max_radius_sq); + bool p1_in_circle_of_p2 = (dist21_sq < max_radius_sq); + + if (!p2_in_circle_of_p1 && !p1_in_circle_of_p2) + continue; + + // Determine which monument is the center (the one that contains the other) + Pos center; + if (p2_in_circle_of_p1) + { + // p1 is center, p2 is within 180 blocks of p1 + center = p1; + } + else + { + // p2 is center, p1 is within 180 blocks of p2 + center = p2; + } + + // Check if at least one monument is within the specified range + bool p1_in_range = false; + bool p2_in_range = false; + + if (rmax) + { + // Circular range check + int64_t dp1x = p1.x - at.x; + int64_t dp1z = p1.z - at.z; + int64_t dp1_sq = dp1x*dp1x + dp1z*dp1z; + p1_in_range = (dp1_sq < rmax); + + int64_t dp2x = p2.x - at.x; + int64_t dp2z = p2.z - at.z; + int64_t dp2_sq = dp2x*dp2x + dp2z*dp2z; + p2_in_range = (dp2_sq < rmax); + } + else + { + // Rectangular range check + p1_in_range = (p1.x >= x1 && p1.x <= x2 && p1.z >= z1 && p1.z <= z2); + p2_in_range = (p2.x >= x1 && p2.x <= x2 && p2.z >= z1 && p2.z <= z2); + } + + // At least one monument must be in range + if (!p1_in_range && !p2_in_range) + continue; + + // In fast pass, we found a valid pair (but haven't checked viability) + // In full passes, we need to verify both monuments are viable + if ((env->searchpass == PASS_FULL_64) || + (env->searchpass == PASS_FULL_48 && !finfo.dep64)) + { + if (*env->stop) return COND_FAILED; + env->init4Dim(finfo.dim); + int id1 = isViableStructurePos(Monument, &env->g, p1.x, p1.z, 0); + int id2 = isViableStructurePos(Monument, &env->g, p2.x, p2.z, 0); + if (!id1 || !id2) + continue; + } + + // Found a valid pair + found = true; + best_center = center; + break; + } + } + + if (found) + { + if (imax == NULL) + { + cent[0] = best_center; + } + else if (*imax > 0) + { + cent[0] = best_center; + *imax = 1; + } + // In fast pass, return MAYBE_VALID since we haven't checked viability + // This allows the seed to proceed to full pass for verification + if (env->searchpass == PASS_FAST_48) + return COND_MAYBE_POS_VALID; + // In full passes, we've already checked viability above + return COND_OK; + } + + // No valid pair found + // In fast pass, if we found monuments but no valid pair, fail immediately + // This is because we've already checked all geometric constraints + if (env->searchpass == PASS_FAST_48) + { + // We found monuments but none form a valid pair - fail + return COND_FAILED; + } + // In full passes, we've checked viability, so if no valid pair, fail + return COND_FAILED; + } case F_DESERT: case F_HUT: @@ -2249,9 +2441,103 @@ void findQuadStructs(int styp, Generator *g, QVector *out) delete[] qlist; } +void findDoubleMonuments(Generator *g, int x1, int z1, int x2, int z2, QVector *out) +{ + StructureConfig sconf; + if (!getStructureConfig_override(Monument, g->mc, &sconf)) + return; - - + // Find all monuments in the search area + int rx1 = x1 >> 9; + int rz1 = z1 >> 9; + int rx2 = x2 >> 9; + int rz2 = z2 >> 9; + + Pos monuments[MAX_INSTANCES]; + int mcnt = 0; + + // Collect all monument positions in the area + for (int rz = rz1; rz <= rz2 && mcnt < MAX_INSTANCES; rz++) + { + for (int rx = rx1; rx <= rx2; rx++) + { + Pos pc; + if (!getStructurePos(Monument, g->mc, g->seed, rx, rz, &pc)) + continue; + + // Check if monument is within search area + if (pc.x < x1 || pc.x > x2 || pc.z < z1 || pc.z > z2) + continue; + + // Check viability + if (!isViableStructurePos(Monument, g, pc.x, pc.z, 0)) + continue; + + monuments[mcnt++] = pc; + } + } + + // Need at least 2 monuments to form a pair + if (mcnt < 2) + return; + + // Check all pairs of monuments + const int64_t max_radius_sq = 180LL * 180LL; + + for (int i = 0; i < mcnt; i++) + { + for (int j = i + 1; j < mcnt; j++) + { + Pos p1 = monuments[i]; + Pos p2 = monuments[j]; + + // Check if x and z differences are NOT 160 and 80 (quad configuration) + int dx = p2.x - p1.x; + int dz = p2.z - p1.z; + int adx = dx < 0 ? -dx : dx; + int adz = dz < 0 ? -dz : dz; + + // Skip if this is a standard quad configuration (160, 80) or (80, 160) + if ((adx == 160 && adz == 80) || (adx == 80 && adz == 160)) + continue; + + // Check if one monument is within 180 blocks of the other + int64_t dx12 = p2.x - p1.x; + int64_t dz12 = p2.z - p1.z; + int64_t dist12_sq = dx12*dx12 + dz12*dz12; + + int64_t dx21 = p1.x - p2.x; + int64_t dz21 = p1.z - p2.z; + int64_t dist21_sq = dx21*dx21 + dz21*dz21; + + // Check if either monument is within 180 blocks of the other + bool p2_in_circle_of_p1 = (dist12_sq < max_radius_sq); + bool p1_in_circle_of_p2 = (dist21_sq < max_radius_sq); + + if (!p2_in_circle_of_p1 && !p1_in_circle_of_p2) + continue; + + // Check if at least one monument is within the specified range + bool p1_in_range = (p1.x >= x1 && p1.x <= x2 && p1.z >= z1 && p1.z <= z2); + bool p2_in_range = (p2.x >= x1 && p2.x <= x2 && p2.z >= z1 && p2.z <= z2); + + // At least one monument must be in range + if (!p1_in_range && !p2_in_range) + continue; + + // Found a valid pair + DoubleInfo dinfo; + dinfo.p[0] = p1; + dinfo.p[1] = p2; + // Center is the average of two monuments + dinfo.center.x = (p1.x + p2.x) / 2; + dinfo.center.z = (p1.z + p2.z) / 2; + dinfo.dx = dx; + dinfo.dz = dz; + out->push_back(dinfo); + } + } +} diff --git a/src/search.h b/src/search.h index 52415c7..f7e2755 100644 --- a/src/search.h +++ b/src/search.h @@ -92,6 +92,7 @@ enum F_BIOME_SAMPLE, F_NOISE_SAMPLE, F_CHAMBERS, + F_DM, // Double Monument - added at end for backwards compatibility // new filters should be added here at the end to keep some downwards compatibility FILTER_MAX, }; @@ -256,6 +257,18 @@ static const struct FilterList : private FilterInfo "location.") }; + list[F_DM] = FilterInfo{ + CAT_STRUCT, 0, LOC_RAD, Monument, 512, BR_FIRST, MC_1_8, MC_NEWEST, 0, 0, disp++, + "monument", + QT_TRANSLATE_NOOP("Filter", "Double Ocean Monument"), + QT_TRANSLATE_NOOP("Filter", + "Requires two ocean monuments where one monument is within 180 blocks " + "of the other (forming a circle centered at one monument). At least one " + "monument must be within the specified search range. The x and z differences " + "between the two monuments must not be 160 and 80 respectively " + "(i.e., this is the only invalid configuration but still valid for the filter).") + }; + list[F_BIOME_SAMPLE] = FilterInfo{ CAT_BIOMES, 1, LOC_RAD, 0, 1, BR_SPLIT, MC_B1_7, MC_NEWEST, 0, 1, disp++, "overworld", @@ -738,7 +751,16 @@ struct QuadInfo float rad; // enclosing radius }; +struct DoubleInfo +{ + Pos p[2]; // two monument positions + Pos center; // center position (average of two monuments) + int dx; // x difference between monuments + int dz; // z difference between monuments +}; + void findQuadStructs(int styp, Generator *g, QVector *out); +void findDoubleMonuments(Generator *g, int x1, int z1, int x2, int z2, QVector *out); #endif // SEARCH_H diff --git a/src/tabslime.cpp b/src/tabslime.cpp new file mode 100644 index 0000000..5cb30aa --- /dev/null +++ b/src/tabslime.cpp @@ -0,0 +1,709 @@ +#include "tabslime.h" +#include "ui_tabslime.h" + +#include "config.h" +#include "message.h" +#include "util.h" +#include "world.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cubiomes/biomes.h" +#include "cubiomes/finders.h" +#include "cubiomes/generator.h" + +// 计算以centerX, centerZ为中心的挂机点,24格之外128格之内的史莱姆区块覆盖面积 +static int calculateSlimeArea(uint64_t seed, int centerX, int centerZ, bool includeBiome, Generator *g, int mc) +{ + const int MIN_DIST = 24; // 24格之外 + const int MAX_DIST = 128; // 128格之内 + const int MIN_DIST2 = MIN_DIST * MIN_DIST; + const int MAX_DIST2 = MAX_DIST * MAX_DIST; + + int totalArea = 0; + + // 计算需要检查的区块范围 + int minChunkX = (centerX - MAX_DIST) >> 4; + int maxChunkX = (centerX + MAX_DIST) >> 4; + int minChunkZ = (centerZ - MAX_DIST) >> 4; + int maxChunkZ = (centerZ + MAX_DIST) >> 4; + + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) + { + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) + { + // 检查是否是史莱姆区块 + if (!isSlimeChunk(seed, chunkX, chunkZ)) + continue; + + // 计算区块的边界坐标 + int chunkMinX = chunkX << 4; + int chunkMaxX = chunkMinX + 15; + int chunkMinZ = chunkZ << 4; + int chunkMaxZ = chunkMinZ + 15; + + // 计算区块四个角点到挂机点的距离平方 + int dx1 = chunkMinX - centerX; + int dz1 = chunkMinZ - centerZ; + int dist2_1 = dx1 * dx1 + dz1 * dz1; + + int dx2 = chunkMaxX - centerX; + int dz2 = chunkMinZ - centerZ; + int dist2_2 = dx2 * dx2 + dz2 * dz2; + + int dx3 = chunkMinX - centerX; + int dz3 = chunkMaxZ - centerZ; + int dist2_3 = dx3 * dx3 + dz3 * dz3; + + int dx4 = chunkMaxX - centerX; + int dz4 = chunkMaxZ - centerZ; + int dist2_4 = dx4 * dx4 + dz4 * dz4; + + // 找出区块四个角点的最小和最大距离平方 + int minDist2 = dist2_1; + if (dist2_2 < minDist2) minDist2 = dist2_2; + if (dist2_3 < minDist2) minDist2 = dist2_3; + if (dist2_4 < minDist2) minDist2 = dist2_4; + + int maxDist2 = dist2_1; + if (dist2_2 > maxDist2) maxDist2 = dist2_2; + if (dist2_3 > maxDist2) maxDist2 = dist2_3; + if (dist2_4 > maxDist2) maxDist2 = dist2_4; + + // 如果区块完全在24圆内或完全在128圆外,跳过 + if (maxDist2 < MIN_DIST2 || minDist2 > MAX_DIST2) + continue; + + // 计算区块内实际在24-128范围内的面积 + // 遍历区块内的每个方块,计算在范围内的面积 + int chunkArea = 0; + for (int z = chunkMinZ; z <= chunkMaxZ; z++) + { + for (int x = chunkMinX; x <= chunkMaxX; x++) + { + int dx = x - centerX; + int dz = z - centerZ; + int dist2 = dx * dx + dz * dz; + + // 只计算在24格之外128格之内的方块 + if (dist2 >= MIN_DIST2 && dist2 <= MAX_DIST2) + { + chunkArea++; + } + } + } + + // 如果没有面积在范围内,跳过 + if (chunkArea == 0) + continue; + + // 应用群系修正 + if (includeBiome && g) + { + // 使用区块中心取样群系 + int chunkCenterX = (chunkX << 4) + 8; + int chunkCenterZ = (chunkZ << 4) + 8; + int biomeId = none; + + if (mc >= MC_1_18) + { + // MC 1.18+:在y=-64取样群系,使用scale=4 + int y = -64 >> 2; // -16 for scale=4 + biomeId = getBiomeAt(g, 4, chunkCenterX >> 2, y, chunkCenterZ >> 2); + } + else + { + // MC 1.17及以前:在y=0取样群系 + biomeId = getBiomeAt(g, 4, chunkCenterX >> 2, 0, chunkCenterZ >> 2); + } + + // 根据版本决定哪些群系需要处理 + // 1.17及以前:仅计算河流和蘑菇岛的减成 + // 1.18:增加溶洞的减成 + // 1.19:增加深暗的减成 + + // 蘑菇岛不算(所有版本) + if (biomeId == mushroom_fields) + { + chunkArea = 0; + } + // 深暗之域不算(1.19+) + else if (mc >= MC_1_19 && biomeId == deep_dark) + { + chunkArea = 0; + } + else + { + // 河流的面积乘以5/6(所有版本) + if (biomeId == river) + { + chunkArea = (chunkArea * 5) / 6; + } + // 溶洞的面积乘以5/6(1.18+) + else if (mc >= MC_1_18 && biomeId == dripstone_caves) + { + chunkArea = (chunkArea * 5) / 6; + } + // Old Growth Pine Taiga的面积乘以0.95(所有版本) + else if (biomeId == old_growth_pine_taiga) + { + chunkArea = (chunkArea * 95) / 100; + } + // 其他群系面积不变 + } + } + + totalArea += chunkArea; + } + } + + return totalArea; +} + +void AnalysisSlime::run() +{ + stop = false; + + Generator g; + setupGenerator(&g, wi.mc, wi.large); + + if (includeBiome) + { + applySeed(&g, DIM_OVERWORLD, wi.seed); + } + + for (idx = 0; idx < (long)seeds.size(); idx++) + { + if (stop) break; + + uint64_t seed = seeds[idx]; + wi.seed = seed; + + if (includeBiome) + { + applySeed(&g, DIM_OVERWORLD, seed); + } + + QList results; + + // 遍历所有可能的挂机点(xz都是16的倍数) + // 确保坐标是16的倍数 + // startX: >= x1 的最小的16的倍数 + // endX: <= x2 的最大的16的倍数 + int startX = (x1 % 16 == 0) ? x1 : ((x1 < 0) ? ((x1 / 16) - 1) * 16 : ((x1 / 16) + 1) * 16); + int startZ = (z1 % 16 == 0) ? z1 : ((z1 < 0) ? ((z1 / 16) - 1) * 16 : ((z1 / 16) + 1) * 16); + int endX = (x2 / 16) * 16; + int endZ = (z2 / 16) * 16; + + for (int centerZ = startZ; centerZ <= endZ && !stop; centerZ += 16) + { + for (int centerX = startX; centerX <= endX && !stop; centerX += 16) + { + int area = calculateSlimeArea(seed, centerX, centerZ, includeBiome, includeBiome ? &g : nullptr, wi.mc); + + if (area >= minArea) + { + SlimeResult result; + result.centerX = centerX; + result.centerZ = centerZ; + result.area = area; + results.append(result); + } + } + } + + if (!results.empty() && !stop) + { + emit seedDone(seed, results); + } + } +} + +TabSlime::TabSlime(MainWindow *parent) + : QWidget(parent) + , ui(new Ui::TabSlime) + , parent(parent) + , thread() +{ + ui->setupUi(this); + + // 设置翻译文本 + ui->labelDescription->setText(tr("Calculate optimal AFK positions for slime chunk coverage (24-128 blocks radius).")); + ui->label->setText(tr("Seed(s):")); + ui->comboSeedSource->setItemText(0, tr("Current seed")); + ui->comboSeedSource->setItemText(1, tr("From matching seeds list")); + ui->labelX1->setText(QString("

X1:

")); + ui->labelZ1->setText(QString("

Z1:

")); + ui->labelX2->setText(QString("

X2:

")); + ui->labelZ2->setText(QString("

Z2:

")); + ui->buttonFromVisible->setText(tr("From visible")); + ui->labelMinArea->setText(tr("Minimum area:")); + ui->checkIncludeBiome->setText(tr("Include biome parameters")); + ui->checkIncludeBiome->setToolTip(tr("

Sample biomes at y=-64 (for MC 1.18+). Exclude mushroom fields and deep dark. Apply multipliers for dripstone caves/rivers (5/6) and old growth pine taiga (0.95).

")); + if (ui->treeResults->headerItem()) + { + ui->treeResults->headerItem()->setText(0, tr("seed")); + ui->treeResults->headerItem()->setText(1, tr("area")); + ui->treeResults->headerItem()->setText(2, tr("x")); + ui->treeResults->headerItem()->setText(3, tr("z")); + } + ui->pushExport->setText(tr("Export...")); + ui->pushStart->setText(tr("Analyze")); + + updt = 20; + nextupdate = 0; + + QIntValidator *intval = new QIntValidator(-60e6, 60e6, this); + ui->lineX1->setValidator(intval); + ui->lineZ1->setValidator(intval); + ui->lineX2->setValidator(intval); + ui->lineZ2->setValidator(intval); + + ui->lineMinArea->setValidator(new QIntValidator(1, INT_MAX, this)); + ui->lineMinArea->setText("10000"); + + ui->treeResults->setSortingEnabled(true); + ui->treeResults->sortByColumn(1, Qt::DescendingOrder); // 按面积降序排列 + + connect(&thread, &AnalysisSlime::seedDone, this, &TabSlime::onAnalysisSeedDone, Qt::BlockingQueuedConnection); + connect(&thread, &AnalysisSlime::finished, this, &TabSlime::onAnalysisFinished); + + // 连接信号槽,实现双向同步 + connect(ui->lineX1, &QLineEdit::editingFinished, this, &TabSlime::on_lineX1_editingFinished); + connect(ui->lineZ1, &QLineEdit::editingFinished, this, &TabSlime::on_lineZ1_editingFinished); + connect(ui->lineX2, &QLineEdit::editingFinished, this, &TabSlime::on_lineX2_editingFinished); + connect(ui->lineZ2, &QLineEdit::editingFinished, this, &TabSlime::on_lineZ2_editingFinished); + connect(ui->comboSeedSource, QOverload::of(&QComboBox::currentIndexChanged), this, &TabSlime::on_comboSeedSource_currentIndexChanged); +} + +TabSlime::~TabSlime() +{ + thread.stop = true; + thread.wait(500); + delete ui; +} + +bool TabSlime::event(QEvent *e) +{ + if (e->type() == QEvent::LayoutRequest) + { + QFontMetrics fm = QFontMetrics(ui->treeResults->font()); + ui->treeResults->setColumnWidth(0, txtWidth(fm) * 24); + ui->treeResults->setColumnWidth(1, txtWidth(fm) * 16); + ui->treeResults->setColumnWidth(2, txtWidth(fm) * 9); + ui->treeResults->setColumnWidth(3, txtWidth(fm) * 9); + } + return QWidget::event(e); +} + +void TabSlime::save(QSettings& settings) +{ + int x1 = ui->lineX1->text().toInt(); + int z1 = ui->lineZ1->text().toInt(); + int x2 = ui->lineX2->text().toInt(); + int z2 = ui->lineZ2->text().toInt(); + int seedsrc = ui->comboSeedSource->currentIndex(); + + // 保存到共享的键,与其他tab同步 + settings.setValue("analysis/x1", x1); + settings.setValue("analysis/z1", z1); + settings.setValue("analysis/x2", x2); + settings.setValue("analysis/z2", z2); + settings.setValue("analysis/seedsrc", seedsrc); + + // 也保存到自己的键 + settings.setValue("slime/x1", x1); + settings.setValue("slime/z1", z1); + settings.setValue("slime/x2", x2); + settings.setValue("slime/z2", z2); + settings.setValue("slime/seedsrc", seedsrc); + settings.setValue("slime/minarea", ui->lineMinArea->text().toInt()); + settings.setValue("slime/includebiome", ui->checkIncludeBiome->isChecked()); +} + +static void loadCheck(QSettings *s, QCheckBox *cb, const char *key) +{ + cb->setChecked(s->value(key, cb->isChecked()).toBool()); +} +static void loadCombo(QSettings *s, QComboBox *combo, const char *key) +{ + combo->setCurrentIndex(s->value(key, combo->currentIndex()).toInt()); +} +static void loadLine(QSettings *s, QLineEdit *line, const char *key) +{ + qlonglong x = line->text().toLongLong(); + line->setText(QString::number(s->value(key, x).toLongLong())); +} + +void TabSlime::load(QSettings& settings) +{ + // 优先从其他tab(如tabbiomes)读取坐标和seeds选项 + QSettings s(APP_STRING, APP_STRING); + int x1 = s.value("analysis/x1", ui->lineX1->text().toInt()).toInt(); + int z1 = s.value("analysis/z1", ui->lineZ1->text().toInt()).toInt(); + int x2 = s.value("analysis/x2", ui->lineX2->text().toInt()).toInt(); + int z2 = s.value("analysis/z2", ui->lineZ2->text().toInt()).toInt(); + int seedsrc = s.value("analysis/seedsrc", ui->comboSeedSource->currentIndex()).toInt(); + + // 如果其他tab有值,使用其他tab的值,否则使用自己的保存值 + if (s.contains("analysis/x1")) + { + ui->lineX1->setText(QString::number(x1)); + ui->lineZ1->setText(QString::number(z1)); + ui->lineX2->setText(QString::number(x2)); + ui->lineZ2->setText(QString::number(z2)); + ui->comboSeedSource->setCurrentIndex(seedsrc); + } + else + { + loadLine(&settings, ui->lineX1, "slime/x1"); + loadLine(&settings, ui->lineZ1, "slime/z1"); + loadLine(&settings, ui->lineX2, "slime/x2"); + loadLine(&settings, ui->lineZ2, "slime/z2"); + loadCombo(&settings, ui->comboSeedSource, "slime/seedsrc"); + } + + loadLine(&settings, ui->lineMinArea, "slime/minarea"); + loadCheck(&settings, ui->checkIncludeBiome, "slime/includebiome"); +} + +void TabSlime::refresh() +{ + // 从其他tab同步坐标和seeds选项 + QSettings settings(APP_STRING, APP_STRING); + if (settings.contains("analysis/x1")) + { + int x1 = settings.value("analysis/x1").toInt(); + int z1 = settings.value("analysis/z1").toInt(); + int x2 = settings.value("analysis/x2").toInt(); + int z2 = settings.value("analysis/z2").toInt(); + int seedsrc = settings.value("analysis/seedsrc", 0).toInt(); + + // 临时断开信号,避免触发保存 + ui->lineX1->blockSignals(true); + ui->lineZ1->blockSignals(true); + ui->lineX2->blockSignals(true); + ui->lineZ2->blockSignals(true); + ui->comboSeedSource->blockSignals(true); + + ui->lineX1->setText(QString::number(x1)); + ui->lineZ1->setText(QString::number(z1)); + ui->lineX2->setText(QString::number(x2)); + ui->lineZ2->setText(QString::number(z2)); + ui->comboSeedSource->setCurrentIndex(seedsrc); + + ui->lineX1->blockSignals(false); + ui->lineZ1->blockSignals(false); + ui->lineX2->blockSignals(false); + ui->lineZ2->blockSignals(false); + ui->comboSeedSource->blockSignals(false); + } +} + +void TabSlime::on_lineX1_editingFinished() +{ + QSettings settings(APP_STRING, APP_STRING); + int x1 = ui->lineX1->text().toInt(); + settings.setValue("analysis/x1", x1); + settings.setValue("slime/x1", x1); +} + +void TabSlime::on_lineZ1_editingFinished() +{ + QSettings settings(APP_STRING, APP_STRING); + int z1 = ui->lineZ1->text().toInt(); + settings.setValue("analysis/z1", z1); + settings.setValue("slime/z1", z1); +} + +void TabSlime::on_lineX2_editingFinished() +{ + QSettings settings(APP_STRING, APP_STRING); + int x2 = ui->lineX2->text().toInt(); + settings.setValue("analysis/x2", x2); + settings.setValue("slime/x2", x2); +} + +void TabSlime::on_lineZ2_editingFinished() +{ + QSettings settings(APP_STRING, APP_STRING); + int z2 = ui->lineZ2->text().toInt(); + settings.setValue("analysis/z2", z2); + settings.setValue("slime/z2", z2); +} + +void TabSlime::on_comboSeedSource_currentIndexChanged(int index) +{ + QSettings settings(APP_STRING, APP_STRING); + settings.setValue("analysis/seedsrc", index); + settings.setValue("slime/seedsrc", index); +} + +void TabSlime::onAnalysisSeedDone(uint64_t seed, QList results) +{ + if (results.empty()) + return; + + // 创建种子父节点 + QTreeWidgetItem *seeditem = new QTreeWidgetItem(); + seeditem->setData(0, Qt::DisplayRole, QVariant::fromValue((qlonglong)seed)); + seeditem->setData(0, Qt::UserRole+0, QVariant::fromValue(seed)); + seeditem->setData(0, Qt::UserRole+1, QVariant::fromValue((int)DIM_OVERWORLD)); + + // 为每个结果创建子节点 + for (const SlimeResult& result : results) + { + QTreeWidgetItem *item = new QTreeWidgetItem(seeditem); + item->setText(0, "-"); + item->setData(1, Qt::DisplayRole, QVariant::fromValue(result.area)); + item->setData(2, Qt::DisplayRole, QVariant::fromValue(result.centerX)); + item->setData(3, Qt::DisplayRole, QVariant::fromValue(result.centerZ)); + item->setData(0, Qt::UserRole+0, QVariant::fromValue(seed)); + item->setData(0, Qt::UserRole+1, QVariant::fromValue((int)DIM_OVERWORLD)); + item->setData(0, Qt::UserRole+2, QVariant::fromValue(Pos{result.centerX, result.centerZ})); + } + + qbufl.push_back(seeditem); + quint64 ns = elapsed.nsecsElapsed(); + if (ns > nextupdate) + { + nextupdate = ns + updt * 1e6; + QTimer::singleShot(updt, this, &TabSlime::onBufferTimeout); + } +} + +void TabSlime::onAnalysisFinished() +{ + onBufferTimeout(); + ui->pushStart->setChecked(false); + ui->pushStart->setText(tr("Analyze")); +} + +void TabSlime::onBufferTimeout() +{ + if (qbufl.empty()) + return; + + uint64_t t = -elapsed.elapsed(); + + ui->treeResults->setSortingEnabled(false); + ui->treeResults->setUpdatesEnabled(false); + ui->treeResults->addTopLevelItems(qbufl); + ui->treeResults->setUpdatesEnabled(true); + ui->treeResults->setSortingEnabled(true); + qbufl.clear(); + + QString progress = QString::asprintf(" (%ld/%zu)", thread.idx.load(), thread.seeds.size()); + ui->pushStart->setText(tr("Stop") + progress); + + QApplication::processEvents(); // force processing of events so we can time correctly + + t += elapsed.elapsed(); + if (8*t > updt) + updt = 4*t; + nextupdate = elapsed.nsecsElapsed() + 1e6 * updt; +} + +void TabSlime::on_pushStart_clicked() +{ + if (thread.isRunning()) + { + thread.stop = true; + return; + } + + // 保存当前设置到共享键 + QSettings settings(APP_STRING, APP_STRING); + save(settings); + + parent->getSeed(&thread.wi); + thread.seeds.clear(); + if (ui->comboSeedSource->currentIndex() == 0) + thread.seeds.push_back(thread.wi.seed); + else + thread.seeds = parent->formControl->getResults(); + + if (thread.seeds.empty()) + { + warn(parent, tr("No seeds to process.")); + return; + } + + int x1 = ui->lineX1->text().toInt(); + int z1 = ui->lineZ1->text().toInt(); + int x2 = ui->lineX2->text().toInt(); + int z2 = ui->lineZ2->text().toInt(); + if (x2 < x1) std::swap(x1, x2); + if (z2 < z1) std::swap(z1, z2); + + thread.x1 = x1; + thread.z1 = z1; + thread.x2 = x2; + thread.z2 = z2; + thread.minArea = ui->lineMinArea->text().toInt(); + thread.includeBiome = ui->checkIncludeBiome->isChecked(); + + if (thread.minArea <= 0) + thread.minArea = 10000; + + ui->treeResults->setSortingEnabled(false); + while (ui->treeResults->topLevelItemCount() > 0) + delete ui->treeResults->takeTopLevelItem(0); + ui->treeResults->setSortingEnabled(true); + + updt = 20; + nextupdate = 0; + elapsed.start(); + + ui->pushExport->setEnabled(false); + ui->pushStart->setChecked(true); + QString progress = QString::asprintf(" (0/%zu)", thread.seeds.size()); + ui->pushStart->setText(tr("Stop") + progress); + thread.start(); +} + +static void csvline(QTextStream& stream, const QString& qte, const QString& sep, QStringList& cols) +{ + if (qte.isEmpty()) + { + for (QString& s : cols) + if (s.contains(sep)) + s = "\"" + s + "\""; + } + stream << qte << cols.join(sep) << qte << "\n"; +} + +void TabSlime::exportResults(QTextStream& stream) +{ + QString qte = parent->config.quote; + QString sep = parent->config.separator; + + stream << "Sep=" + sep + "\n"; + sep = qte + sep + qte; + + QStringList header = { tr("seed"), tr("area"), tr("x"), tr("z") }; + csvline(stream, qte, sep, header); + + QTreeWidgetItemIterator it(ui->treeResults); + QString seed; + for (; *it; ++it) + { + QTreeWidgetItem *item = *it; + if (item->text(0) != "-") + { + seed = item->text(0); + continue; + } + QStringList cols; + cols.append(seed); + cols.append(item->text(1)); + cols.append(item->text(2)); + cols.append(item->text(3)); + csvline(stream, qte, sep, cols); + } + stream.flush(); +} + +void TabSlime::on_pushExport_clicked() +{ +#if WASM + QByteArray content; + QTextStream stream(&content); + exportResults(stream); + QFileDialog::saveFileContent(content, "slime.csv"); +#else + QString fnam = QFileDialog::getSaveFileName( + this, tr("Export slime results"), parent->prevdir, tr("Text files (*.txt *.csv);;Any files (*)")); + if (fnam.isEmpty()) + return; + + QFileInfo finfo(fnam); + QFile file(fnam); + parent->prevdir = finfo.absolutePath(); + + if (!file.open(QIODevice::WriteOnly)) + { + warn(parent, tr("Failed to open file for export:\n\"%1\"").arg(fnam)); + return; + } + + QTextStream stream(&file); + exportResults(stream); +#endif +} + +void TabSlime::on_buttonFromVisible_clicked() +{ + MapView *mapview = parent->getMapView(); + + int x1, z1, x2, z2; + mapview->getVisible(&x1, &z1, &x2, &z2); + + ui->lineX1->setText(QString::number(x1)); + ui->lineZ1->setText(QString::number(z1)); + ui->lineX2->setText(QString::number(x2)); + ui->lineZ2->setText(QString::number(z2)); +} + +void TabSlime::on_treeResults_itemClicked(QTreeWidgetItem *item, int column) +{ + (void) column; + QVariant dat; + dat = item->data(0, Qt::UserRole); + if (dat.isValid()) + { + uint64_t seed = qvariant_cast(dat); + int dim = item->data(0, Qt::UserRole+1).toInt(); + WorldInfo wi; + parent->getSeed(&wi); + if (wi.seed != seed || (dim != DIM_UNDEF && dim != parent->getDim())) + { + wi.seed = seed; + parent->getMapView()->deleteWorld(); + } + parent->setSeed(wi, dim); + } + + dat = item->data(0, Qt::UserRole+2); + if (dat.isValid()) + { + Pos p = qvariant_cast(dat); + + // 创建两个圆形:半径为128和24 + currentShapes.clear(); + + Shape circle128; + circle128.type = Shape::CIRCLE; + circle128.dim = DIM_OVERWORLD; + circle128.p1 = p; + circle128.p2 = Pos{0, 0}; + circle128.r = 128; + currentShapes.push_back(circle128); + + Shape circle24; + circle24.type = Shape::CIRCLE; + circle24.dim = DIM_OVERWORLD; + circle24.p1 = p; + circle24.p2 = Pos{0, 0}; + circle24.r = 24; + currentShapes.push_back(circle24); + + // 跳转到坐标并设置形状 + parent->getMapView()->setView(p.x+0.5, p.z+0.5); + parent->getMapView()->setShapes(currentShapes); + } +} + +void TabSlime::clearSlimeShapes() +{ + currentShapes.clear(); + parent->getMapView()->setShapes(currentShapes); +} + diff --git a/src/tabslime.h b/src/tabslime.h new file mode 100644 index 0000000..893b852 --- /dev/null +++ b/src/tabslime.h @@ -0,0 +1,92 @@ +#ifndef TABSLIME_H +#define TABSLIME_H + +#include +#include +#include + +#include "mainwindow.h" +#include "util.h" + +namespace Ui { +class TabSlime; +} + +struct SlimeResult +{ + int centerX, centerZ; // 挂机点坐标(16的倍数) + int area; // 有效史莱姆区块刷怪面积 +}; + +class AnalysisSlime : public QThread +{ + Q_OBJECT +public: + explicit AnalysisSlime(QObject *parent = nullptr) + : QThread(parent), idx() {} + + virtual void run() override; + +signals: + void seedDone(uint64_t seed, QList results); + +public: + std::vector seeds; + WorldInfo wi; + std::atomic_bool stop; + std::atomic_long idx; + int x1, z1, x2, z2; // 挂机位置坐标范围 + int minArea; // 最小面积阈值 + bool includeBiome; // 是否包含群系计算 +}; + +class TabSlime : public QWidget, public ISaveTab +{ + Q_OBJECT + +public: + explicit TabSlime(MainWindow *parent = nullptr); + ~TabSlime(); + + virtual bool event(QEvent *e) override; + + virtual void save(QSettings& settings) override; + virtual void load(QSettings& settings) override; + virtual void refresh() override; + + void clearSlimeShapes(); // 清除史莱姆坐标的红圈 + +private slots: + void onAnalysisSeedDone(uint64_t seed, QList results); + void onAnalysisFinished(); + void onBufferTimeout(); + + void on_pushStart_clicked(); + void on_pushExport_clicked(); + void on_buttonFromVisible_clicked(); + void on_treeResults_itemClicked(QTreeWidgetItem *item, int column); + + void on_lineX1_editingFinished(); + void on_lineZ1_editingFinished(); + void on_lineX2_editingFinished(); + void on_lineZ2_editingFinished(); + void on_comboSeedSource_currentIndexChanged(int index); + +private: + void exportResults(QTextStream& stream); + +private: + Ui::TabSlime *ui; + MainWindow *parent; + AnalysisSlime thread; + + QElapsedTimer elapsed; + uint64_t updt; + uint64_t nextupdate; + QList qbufl; + + std::vector currentShapes; // 当前绘制的形状 +}; + +#endif // TABSLIME_H + diff --git a/src/tabslime.ui b/src/tabslime.ui new file mode 100644 index 0000000..1732a40 --- /dev/null +++ b/src/tabslime.ui @@ -0,0 +1,278 @@ + + + TabSlime + + + + 0 + 0 + 549 + 353 + + + + Form + + + + + + + + false + + + Export... + + + + + + + Analyze + + + + + + + + + Minimum area: + + + + + + + Calculate optimal AFK positions for slime chunk coverage (24-128 blocks radius). + + + + + + + + Monospace + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::ScrollPerPixel + + + true + + + true + + + + seed + + + + Monospace + + + + + + area + + + + Monospace + + + + + + x + + + + Monospace + + + + + + z + + + + Monospace + + + + + + + + + + + + <html><head/></head><body><p>Sample biomes at y=-64 (for MC 1.18+). Exclude mushroom fields and deep dark. Apply multipliers for dripstone caves/rivers (5/6) and old growth pine taiga (0.95).</p></body></html> + + + Include biome parameters + + + + + + + 10000 + + + + + + + Seed(s): + + + + + + + + + + + Lower bound (inclusive) + + + <html><head/><body><p>X<span style=" vertical-align:sub;">1</span>:</p></body></html> + + + + + + + Lower bound (inclusive) + + + 0 + + + + + + + Lower bound (inclusive) + + + <html><head/><body><p>Z<span style=" vertical-align:sub;">1</span>:</p></body></html> + + + + + + + Lower bound (inclusive) + + + 0 + + + + + + + + + + + + + + Upper bound (inclusive) + + + <html><head/><body><p>X<span style=" vertical-align:sub;">2</span>:</p></body></html> + + + + + + + Upper bound (inclusive) + + + 0 + + + + + + + Upper bound (inclusive) + + + <html><head/><body><p>Z<span style=" vertical-align:sub;">2</span>:</p></body></html> + + + + + + + Upper bound (inclusive) + + + 0 + + + + + + + + + From visible + + + + + + + + + + 0 + 0 + + + + + Current seed + + + + + From matching seeds list + + + + + + + + + CoordEdit + QLineEdit +
src/conditiondialog.h
+
+ + StyledComboBox + QComboBox +
src/widgets.h
+
+
+ + +
diff --git a/src/tabstructures.cpp b/src/tabstructures.cpp index 9fb48ac..d350f23 100644 --- a/src/tabstructures.cpp +++ b/src/tabstructures.cpp @@ -2,6 +2,7 @@ #include "ui_tabstructures.h" #include "message.h" +#include "search.h" #include "util.h" #include @@ -41,6 +42,8 @@ void AnalysisStructures::run() wi.seed = seeds[idx]; if (quad) runQuads(&g); + else if (this->isDouble) + runDoubles(&g); else runStructs(&g); } @@ -215,6 +218,42 @@ void AnalysisStructures::runQuads(Generator *g) emit quadDone(seeditem); } +void AnalysisStructures::runDoubles(Generator *g) +{ + applySeed(g, 0, wi.seed); + + QVector dsinfo; + findDoubleMonuments(g, area.x1, area.z1, area.x2, area.z2, &dsinfo); + if (dsinfo.empty()) + return; + + QTreeWidgetItem *seeditem = new TreeIntItem(); + seeditem->setText(0, QString::asprintf("%" PRId64, wi.seed)); + seeditem->setData(0, Qt::UserRole+0, QVariant::fromValue(wi.seed)); + seeditem->setData(0, Qt::UserRole+1, QVariant::fromValue((int)DIM_OVERWORLD)); + + for (DoubleInfo& di : dsinfo) + { + QTreeWidgetItem *item = new QTreeWidgetItem(seeditem); + + qreal dist = di.center.x*(qreal)di.center.x + di.center.z*(qreal)di.center.z; + dist = sqrt(dist); + + item->setText(0, "-"); + item->setData(1, Qt::DisplayRole, QVariant::fromValue(QString("double-monument"))); + item->setData(2, Qt::DisplayRole, QVariant::fromValue((qlonglong)dist)); + item->setData(3, Qt::DisplayRole, QVariant::fromValue(di.center.x)); + item->setData(4, Qt::DisplayRole, QVariant::fromValue(di.center.z)); + item->setData(5, Qt::DisplayRole, QVariant::fromValue(di.dx)); + item->setData(6, Qt::DisplayRole, QVariant::fromValue(di.dz)); + item->setData(0, Qt::UserRole+0, QVariant::fromValue(wi.seed)); + item->setData(0, Qt::UserRole+1, QVariant::fromValue((int)DIM_OVERWORLD)); + item->setData(0, Qt::UserRole+2, QVariant::fromValue(di.center)); + } + + emit doubleDone(seeditem); +} + TabStructures::TabStructures(MainWindow *parent) : QWidget(parent) @@ -223,6 +262,7 @@ TabStructures::TabStructures(MainWindow *parent) , thread(this) , sortcols(-1) , sortcolq(-1) + , sortcold(-1) , nextupdate() , updt(100) { @@ -235,12 +275,18 @@ TabStructures::TabStructures(MainWindow *parent) ui->treeQuads->sortByColumn(-1, Qt::AscendingOrder); connect(ui->treeQuads->header(), &QHeaderView::sectionClicked, this, [=](){ onHeaderClick(ui->treeQuads); } ); + ui->treeDoubles->setColumnWidth(0, 160); + ui->treeDoubles->sortByColumn(-1, Qt::AscendingOrder); + connect(ui->treeDoubles->header(), &QHeaderView::sectionClicked, this, [=](){ onHeaderClick(ui->treeDoubles); } ); + connect(&thread, &AnalysisStructures::itemDone, this, &TabStructures::onAnalysisItemDone, Qt::BlockingQueuedConnection); connect(&thread, &AnalysisStructures::quadDone, this, &TabStructures::onAnalysisQuadDone, Qt::BlockingQueuedConnection); + connect(&thread, &AnalysisStructures::doubleDone, this, &TabStructures::onAnalysisDoubleDone, Qt::BlockingQueuedConnection); connect(&thread, &AnalysisStructures::finished, this, &TabStructures::onAnalysisFinished); connect(ui->treeStructs, &QTreeWidget::itemClicked, this, &TabStructures::onTreeItemClicked); connect(ui->treeQuads, &QTreeWidget::itemClicked, this, &TabStructures::onTreeItemClicked); + connect(ui->treeDoubles, &QTreeWidget::itemClicked, this, &TabStructures::onTreeItemClicked); } TabStructures::~TabStructures() @@ -268,6 +314,14 @@ bool TabStructures::event(QEvent *e) ui->treeQuads->setColumnWidth(4, txtWidth(fm) * 10); ui->treeQuads->setColumnWidth(5, txtWidth(fm, "_123.123")); ui->treeQuads->setColumnWidth(6, txtWidth(fm) * 14); + + ui->treeDoubles->setColumnWidth(0, txtWidth(fm) * 23); + ui->treeDoubles->setColumnWidth(1, txtWidth(fm, "_double-monument")); + ui->treeDoubles->setColumnWidth(2, txtWidth(fm) * 10); + ui->treeDoubles->setColumnWidth(3, txtWidth(fm) * 10); + ui->treeDoubles->setColumnWidth(4, txtWidth(fm) * 10); + ui->treeDoubles->setColumnWidth(5, txtWidth(fm) * 10); + ui->treeDoubles->setColumnWidth(6, txtWidth(fm) * 10); } return QWidget::event(e); } @@ -312,7 +366,7 @@ void TabStructures::load(QSettings& settings) void TabStructures::onHeaderClick(QTreeView *tree) { - int& col = (tree == ui->treeStructs) ? sortcols : sortcolq; + int& col = (tree == ui->treeStructs) ? sortcols : ((tree == ui->treeQuads) ? sortcolq : sortcold); int section = tree->header()->sortIndicatorSection(); if (tree->header()->sortIndicatorOrder() == Qt::AscendingOrder && col == section) { @@ -344,19 +398,31 @@ void TabStructures::onAnalysisQuadDone(QTreeWidgetItem *item) } } +void TabStructures::onAnalysisDoubleDone(QTreeWidgetItem *item) +{ + qbufd.push_back(item); + quint64 ns = elapsed.nsecsElapsed(); + if (ns > nextupdate) + { + nextupdate = ns + updt * 1e6; + QTimer::singleShot(updt, this, &TabStructures::onBufferTimeout); + } +} + void TabStructures::onAnalysisFinished() { onBufferTimeout(); on_tabWidget_currentChanged(-1); ui->treeStructs->setSortingEnabled(true); ui->treeQuads->setSortingEnabled(true); + ui->treeDoubles->setSortingEnabled(true); ui->pushStart->setChecked(false); ui->pushStart->setText(tr("Analyze")); } void TabStructures::onBufferTimeout() { - if (qbufs.empty() && qbufq.empty()) + if (qbufs.empty() && qbufq.empty() && qbufd.empty()) return; uint64_t t = -elapsed.elapsed(); if (!qbufs.empty()) @@ -380,6 +446,17 @@ void TabStructures::onBufferTimeout() ui->treeQuads->setSortingEnabled(true); qbufq.clear(); } + if (!qbufd.empty()) + { + ui->treeDoubles->setSortingEnabled(false); + ui->treeDoubles->setUpdatesEnabled(false); + ui->treeDoubles->addTopLevelItems(qbufd); + for (QTreeWidgetItem *item: qAsConst(qbufd)) + item->setExpanded(true); + ui->treeDoubles->setUpdatesEnabled(true); + ui->treeDoubles->setSortingEnabled(true); + qbufd.clear(); + } QString progress = QString::asprintf(" (%d/%zu)", thread.idx.load(), thread.seeds.size()); ui->pushStart->setText(tr("Stop") + progress); @@ -458,21 +535,33 @@ void TabStructures::on_pushStart_clicked() if (ui->tabWidget->currentWidget() == ui->tabStructures) { thread.quad = false; + thread.isDouble = false; dats = thread.area; ui->treeStructs->setSortingEnabled(false); while (ui->treeStructs->topLevelItemCount() > 0) delete ui->treeStructs->takeTopLevelItem(0); ui->treeStructs->setSortingEnabled(true); } - else + else if (ui->tabWidget->currentWidget() == ui->tabQuads) { thread.quad = true; + thread.isDouble = false; datq = thread.area; ui->treeQuads->setSortingEnabled(false); while (ui->treeQuads->topLevelItemCount() > 0) delete ui->treeQuads->takeTopLevelItem(0); ui->treeQuads->setSortingEnabled(true); } + else if (ui->tabWidget->currentWidget() == ui->tabDoubles) + { + thread.quad = false; + thread.isDouble = true; + datd = thread.area; + ui->treeDoubles->setSortingEnabled(false); + while (ui->treeDoubles->topLevelItemCount() > 0) + delete ui->treeDoubles->takeTopLevelItem(0); + ui->treeDoubles->setSortingEnabled(true); + } ui->pushExport->setEnabled(false); ui->pushStart->setChecked(true); @@ -593,6 +682,30 @@ void TabStructures::exportResults(QTextStream& stream) csvline(stream, qte, sep, cols); } } + else if(ui->tabWidget->currentWidget() == ui->tabDoubles) + { + stream << qte << "#X1" << sep << datd.x1 << qte << "\n"; + stream << qte << "#Z1" << sep << datd.z1 << qte << "\n"; + stream << qte << "#X2" << sep << datd.x2 << qte << "\n"; + stream << qte << "#Z2" << sep << datd.z2 << qte << "\n"; + + QStringList header = { tr("seed"), tr("type"), tr("distance"), tr("x"), tr("z"), tr("dx"), tr("dz") }; + csvline(stream, qte, sep, header); + QString seed; + for (QTreeWidgetItemIterator it(ui->treeDoubles); *it; ++it) + { + QTreeWidgetItem *item = *it; + if (item->text(0) != "-") + { + seed = item->text(0); + continue; + } + QStringList cols = { seed }; + for (int i = 1, n = item->columnCount(); i < n; i++) + cols.append(item->text(i)); + csvline(stream, qte, sep, cols); + } + } stream.flush(); } @@ -644,8 +757,10 @@ void TabStructures::on_tabWidget_currentChanged(int) { if (ui->tabWidget->currentWidget() == ui->tabStructures) ok = ui->treeStructs->topLevelItemCount() > 0; - if (ui->tabWidget->currentWidget() == ui->tabQuads) + else if (ui->tabWidget->currentWidget() == ui->tabQuads) ok = ui->treeQuads->topLevelItemCount() > 0; + else if (ui->tabWidget->currentWidget() == ui->tabDoubles) + ok = ui->treeDoubles->topLevelItemCount() > 0; } ui->pushExport->setEnabled(ok); } diff --git a/src/tabstructures.h b/src/tabstructures.h index fb4645c..c3dadb0 100644 --- a/src/tabstructures.h +++ b/src/tabstructures.h @@ -17,10 +17,12 @@ class AnalysisStructures : public QThread virtual void run() override; void runStructs(Generator *g); void runQuads(Generator *g); + void runDoubles(Generator *g); signals: void itemDone(QTreeWidgetItem *item); void quadDone(QTreeWidgetItem *item); + void doubleDone(QTreeWidgetItem *item); public: std::vector seeds; @@ -32,6 +34,7 @@ class AnalysisStructures : public QThread bool mapshow[D_STRUCT_NUM]; bool collect; bool quad; + bool isDouble; }; class TabStructures : public QWidget, public ISaveTab @@ -52,6 +55,7 @@ private slots: void onAnalysisItemDone(QTreeWidgetItem *item); void onAnalysisQuadDone(QTreeWidgetItem *item); + void onAnalysisDoubleDone(QTreeWidgetItem *item); void onAnalysisFinished(); void onBufferTimeout(); @@ -69,14 +73,15 @@ private slots: Ui::TabStructures *ui; MainWindow *parent; AnalysisStructures thread; - AnalysisStructures::Dat dats, datq; - int sortcols, sortcolq; + AnalysisStructures::Dat dats, datq, datd; + int sortcols, sortcolq, sortcold; QElapsedTimer elapsed; uint64_t nextupdate; uint64_t updt; QList qbufs; QList qbufq; + QList qbufd; }; #endif // TABSTRUCTURES_H diff --git a/src/tabstructures.ui b/src/tabstructures.ui index 69f9f2e..e81fcb6 100644 --- a/src/tabstructures.ui +++ b/src/tabstructures.ui @@ -227,6 +227,81 @@
+ + + Double-Structures + + + + + + + Monospace + + + + Qt::CustomContextMenu + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + true + + + true + + + + seed + + + + + type + + + + + distance + + + + + x + + + + + z + + + + + dx + + + + + dz + + + + + +