├── .github └── workflows │ └── gradle-publish.yml ├── .gitignore ├── .profileconfig.json ├── LICENSE ├── README.md ├── SyncBundles.java ├── assets ├── bundles │ ├── bundle.properties │ ├── bundle_de_DE.properties │ ├── bundle_ja.properties │ ├── bundle_ru.properties │ ├── bundle_zh_CN.properties │ └── bundle_zh_TW.properties ├── git │ ├── binding.png │ ├── code_structure.png │ ├── hotkey.png │ ├── preview.png │ ├── schematic_designer.png │ └── tmibutton.png └── sprites │ └── ui │ ├── a_z.png │ ├── balance.png │ ├── clip.png │ ├── inbalance.png │ ├── panner.png │ ├── side_bottom.png │ ├── side_left.png │ ├── side_right.png │ ├── side_top.png │ ├── time.png │ ├── tmi.png │ ├── ui-bottom-left.png │ ├── ui-bottom-right.png │ ├── ui-bottom.png │ ├── ui-center.png │ ├── ui-left.png │ ├── ui-right.png │ ├── ui-top-left.png │ ├── ui-top-right.png │ └── ui-top.png ├── build.gradle.kts ├── bundleInfo.properties ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── hs_err_pid481079.log ├── icon.png ├── jitpack.yml ├── mod.hjson ├── settings.gradle.kts └── src ├── main └── kotlin │ └── tmi │ ├── ModAPI.kt │ ├── RecipeEntry.kt │ ├── RecipeEntryPoint.kt │ ├── TooManyItems.kt │ ├── recipe │ ├── AmountFormatter.kt │ ├── EnvParameter.kt │ ├── Recipe.kt │ ├── RecipeItemManager.kt │ ├── RecipeItemStack.kt │ ├── RecipeParser.kt │ ├── RecipeType.kt │ ├── RecipesManager.kt │ ├── parser │ │ ├── AttributeCrafterParser.kt │ │ ├── BeamDrillParser.kt │ │ ├── ConsGeneratorParser.kt │ │ ├── ConstructorParser.kt │ │ ├── ConsumerParser.kt │ │ ├── DrillParser.kt │ │ ├── FrackerParser.kt │ │ ├── GeneratorParser.kt │ │ ├── GenericCrafterParser.kt │ │ ├── HeatCrafterParser.kt │ │ ├── HeatGeneratorParser.kt │ │ ├── HeatProducerParser.kt │ │ ├── PumpParser.kt │ │ ├── ReconstructorParser.kt │ │ ├── SeparatorParser.kt │ │ ├── SolidPumpParser.kt │ │ ├── ThermalGeneratorParser.kt │ │ ├── UnitAssemblerParser.kt │ │ ├── UnitFactoryParser.kt │ │ ├── VariableReactorParser.kt │ │ └── WallCrafterParser.kt │ └── types │ │ ├── BuildingRecipe.kt │ │ ├── CollectingRecipe.kt │ │ ├── FactoryRecipe.kt │ │ ├── GeneratorRecipe.kt │ │ ├── HeatMark.kt │ │ ├── PowerMark.kt │ │ ├── RecipeItem.kt │ │ └── SingleItemMark.kt │ ├── ui │ ├── Cursor.kt │ ├── EntryAssigner.kt │ ├── NodeType.kt │ ├── RecipeNode.kt │ ├── RecipeView.kt │ ├── RecipesDialog.kt │ ├── Side.kt │ ├── TmiUI.kt │ ├── designer │ │ ├── BalanceStatistic.kt │ │ ├── Card.kt │ │ ├── DesignerHandle.kt │ │ ├── DesignerView.kt │ │ ├── Events.kt │ │ ├── ExportDialog.kt │ │ ├── FoldIconCfgDialog.kt │ │ ├── FoldLink.kt │ │ ├── IOCard.kt │ │ ├── ItemLinker.kt │ │ ├── PieChartSetter.kt │ │ ├── RecipeCard.kt │ │ ├── SchematicDesignerDialog.kt │ │ ├── SchematicGraphDrawer.kt │ │ └── StatisticEvent.kt │ └── element │ │ └── Flex.java │ └── util │ ├── CombineKeyListener.kt │ ├── CombineKeyTree.kt │ ├── CombinedKeys.kt │ ├── Consts.kt │ ├── Geom.kt │ ├── KeyBinds.kt │ ├── Shapes.kt │ ├── Temps.kt │ ├── Utils.kt │ └── WrapAppListener.kt └── test └── kotlin └── Test.kt /.github/workflows/gradle-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created 6 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle 7 | 8 | name: Gradle Package 9 | 10 | on: 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up JDK 17 24 | uses: actions/setup-java@v4 25 | with: 26 | java-version: '17' 27 | distribution: 'temurin' 28 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 29 | settings-path: ${{ github.workspace }} # location for the settings.xml file 30 | 31 | - name: Setup Gradle 32 | uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 33 | 34 | - name: Build with Gradle 35 | run: ./gradlew build 36 | 37 | # The USERNAME and TOKEN need to correspond to the credentials environment variables used in 38 | # the publishing section of your build.gradle 39 | - name: Publish to GitHub Packages 40 | run: ./gradlew publish 41 | env: 42 | USERNAME: ${{ github.actor }} 43 | TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | .idea/ 41 | 42 | ### Mac OS ### 43 | .DS_Store -------------------------------------------------------------------------------- /.profileconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "jfrConfig": { 3 | "settings": "profile" 4 | }, 5 | "asyncProfilerConfig": { 6 | "jfrsync": true, 7 | "alloc": true, 8 | "event": "wall", 9 | "misc": "" 10 | }, 11 | "file": "$PROJECT_DIR/profile.jfr", 12 | "conversionConfig": { 13 | "nonProjectPackagePrefixes": [ 14 | "java.", 15 | "javax.", 16 | "kotlin.", 17 | "jdk.", 18 | "com.google.", 19 | "org.apache.", 20 | "org.spring.", 21 | "sun.", 22 | "scala." 23 | ], 24 | "enableMarkers": true, 25 | "initialVisibleThreads": 10, 26 | "initialSelectedThreads": 10, 27 | "includeGCThreads": false, 28 | "includeInitialSystemProperty": false, 29 | "includeInitialEnvironmentVariables": false, 30 | "includeSystemProcesses": false, 31 | "ignoredEvents": [ 32 | "jdk.ActiveSetting", 33 | "jdk.ActiveRecording", 34 | "jdk.BooleanFlag", 35 | "jdk.IntFlag", 36 | "jdk.DoubleFlag", 37 | "jdk.LongFlag", 38 | "jdk.NativeLibrary", 39 | "jdk.StringFlag", 40 | "jdk.UnsignedIntFlag", 41 | "jdk.UnsignedLongFlag", 42 | "jdk.InitialSystemProperty", 43 | "jdk.InitialEnvironmentVariable", 44 | "jdk.SystemProcess", 45 | "jdk.ModuleExport", 46 | "jdk.ModuleRequire" 47 | ], 48 | "minRequiredItemsPerThread": 3 49 | }, 50 | "additionalGradleTargets": [ 51 | { 52 | "targetPrefix": "quarkus", 53 | "optionForVmArgs": "-Djvm.args", 54 | "description": "Example quarkus config, adding profiling arguments via -Djvm.args option to the Gradle task run" 55 | } 56 | ], 57 | "additionalMavenTargets": [ 58 | { 59 | "targetPrefix": "quarkus:", 60 | "optionForVmArgs": "-Djvm.args", 61 | "description": "Example quarkus config, adding profiling arguments via -Djvm.args option to the Maven goal run" 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Too Many Items 物品管理器(TMI) 2 | 3 | --- 4 | 5 | 本mod旨在提供一个便捷的界面,以供快速查询物品的生产与制造信息,在您安装了很多mod的情况下会非常有用。 6 | 7 | ![preview](assets/git/preview.png) 8 | 9 | 就像上面所展示的那样,您可以简单的从列表中选择和查询您想要了解的项目制造信息,这包括某一项目的用途,获取方式或者某个方块的建筑成本和工厂可用于的加工过程等。 10 | 11 | - 在您安装该mod之后,您的游戏页面会新增一个可以拖动的按钮(原始状态下会显示在左下角),单击此按钮即可直接打开TMI物品管理器页面。 12 | - 此外,在核心数据库内也会新增一个`TMI物品管理器`按钮,该按钮同样可以打开该页面。 13 | - 在物品的详细信息中,如果物品具有配方属性,那么将会在详细信息页添加一个按钮直接跳转到该物品的配方页面。 14 | 15 | 而所有的按钮均如下: 16 | 17 | ![button](assets/git/tmibutton.png) 18 | 19 | 通常来说,这个mod支持所有的基于json的mod和大部分没有过多自定义生产类型的第三方mod,如果第三方mod有非基于vanilla的加工或者制造流程,那么其可能需要作者做一些适配,请参阅[本项目Wiki](https://github.com/EB-wilson/TooManyItems/wiki)。 20 | 21 | --- 22 | 23 | ### 关于热键和触屏操作 24 | 25 | 如上所示的那个页面,左右两侧的每一个物件都可以通过单击打开其获取方式。若要查看其用途,在键鼠操作模式下,TMI有一个新增的热键绑定在键位设置中: 26 | 27 | ![img.png](assets/git/binding.png) 28 | 29 | 在按住此热键时单击项目即查询项目的用途。 30 | 31 | 若您使用的是触屏设备,则通过快速双击项目以查询其用途。无论是触控还是键鼠,您都可以长按条目一秒以打开项目的详细信息(如果有的话),长按一秒的进度也会以动画的形式呈现。 32 | -------------------------------------------------------------------------------- /SyncBundles.java: -------------------------------------------------------------------------------- 1 | import java.io.*; 2 | import java.util.ArrayList; 3 | import java.util.LinkedHashMap; 4 | import java.util.Locale; 5 | import java.util.regex.Pattern; 6 | import java.text.DateFormat; 7 | import java.util.Date; 8 | import java.nio.charset.StandardCharsets; 9 | 10 | public class SyncBundles{ 11 | private static final Pattern matcher = Pattern.compile("\\\\\\n"); 12 | public static final char SEP = '\\'; 13 | 14 | public static void main(String[] args){ 15 | Properties info = new Properties(new File("bundleInfo.properties")); 16 | String sourceLocale = info.get("source"); 17 | String bundlesDir = info.get("bundlesDir").replace("/", File.separator); 18 | String modName = info.get("modName"); 19 | Properties loc = new Properties(); 20 | loc.read(info.getOrigin("targetLocales").replace("[", "").replace("]", "").replace(SEP + System.lineSeparator(), System.lineSeparator())); 21 | String[] locales = new String[loc.map.size()*2]; 22 | int index = 0; 23 | for(Pair entry: loc.map.values()){ 24 | locales[index] = entry.key.replace("en_US", ""); 25 | locales[index+1] = entry.value; 26 | index += 2; 27 | } 28 | 29 | File source = new File(bundlesDir, "bundle" + (sourceLocale.isBlank() ? "": "_" + sourceLocale) + ".properties"); 30 | Properties sourceBundle = new Properties(); 31 | sourceBundle.read(source); 32 | 33 | handleHeader(sourceBundle, modName, sourceLocale, args); 34 | 35 | sourceBundle.write(source); 36 | for(int i = 0; i < locales.length; i += 2){ 37 | String locale = locales[i], mark = locales[i + 1]; 38 | File file = new File(bundlesDir, "bundle" + (locale.isBlank() ? "": "_" + locale) + ".properties"); 39 | Properties bundle = new Properties(sourceBundle, mark); 40 | if(file.exists()) bundle.read(file); 41 | handleHeader(bundle, modName, locale, args); 42 | bundle.write(file); 43 | } 44 | } 45 | 46 | public static void handleHeader(Properties source, String name, String locTag, String... args){ 47 | source.put(name + ".mod.updateDate", DateFormat.getDateInstance(DateFormat.DEFAULT, Locale.forLanguageTag(locTag.replace("_", "-"))).format(new Date()), 0); 48 | 49 | source.put(name + ".mod.version", args[0], 0); 50 | } 51 | 52 | public static class Properties{ 53 | ArrayList lines = new ArrayList<>(); 54 | LinkedHashMap map = new LinkedHashMap<>(); 55 | 56 | String mark; 57 | 58 | public Properties(){} 59 | 60 | public Properties(File file){ 61 | read(file); 62 | } 63 | 64 | public Properties(Properties source, String mark){ 65 | this.mark = mark; 66 | for(Line line: source.lines){ 67 | Line l; 68 | if(line instanceof Pair){ 69 | l = new Pair(line.string, ((Pair) line).key, mark + " " + ((Pair) line).value); 70 | map.put(((Pair) l).key, (Pair) l); 71 | } 72 | else l = new Line(line.string); 73 | lines.add(l); 74 | } 75 | } 76 | 77 | public void put(String lineStr, int line){ 78 | lines.add(line, new Line(lineStr)); 79 | } 80 | 81 | public void put(String key, String value, int line){ 82 | key = string2Unicode(key); 83 | String v = string2Unicode(value); 84 | 85 | Pair pair = map.computeIfAbsent(key, k -> { 86 | Pair r = new Pair(k + " = " + v, k, v); 87 | lines.add(line, r); 88 | 89 | return r; 90 | }); 91 | pair.value = v; 92 | } 93 | 94 | public String get(String key){ 95 | String str = map.containsKey(key)? map.get(key).value: null; 96 | StringBuilder result = new StringBuilder(); 97 | 98 | for(String res: matcher.split(str)){ 99 | result.append(res); 100 | } 101 | 102 | return result.toString(); 103 | } 104 | 105 | public String getOrigin(String key){ 106 | return map.containsKey(key)? map.get(key).value: null; 107 | } 108 | 109 | public void read(String content){ 110 | read(new BufferedReader(new StringReader(content))); 111 | } 112 | 113 | public void read(File file){ 114 | try{ 115 | read(new BufferedReader(new FileReader(file))); 116 | }catch(FileNotFoundException e){ 117 | e.printStackTrace(); 118 | } 119 | } 120 | 121 | public void read(BufferedReader reader){ 122 | try{ 123 | String line; 124 | boolean init = lines.isEmpty(); 125 | Pair last = null; 126 | 127 | while((line = reader.readLine()) != null){ 128 | if(last != null){ 129 | last.value = last.value + line; 130 | 131 | if(line.endsWith(String.valueOf(SEP))){ 132 | last.value += System.lineSeparator(); 133 | } 134 | else last = null; 135 | 136 | continue; 137 | } 138 | 139 | String[] strs = line.trim().split("=", 2); 140 | if(init){ 141 | if(!strs[0].startsWith("#") && strs.length == 2){ 142 | Pair p; 143 | lines.add(p = new Pair(line, strs[0].trim(), strs[1].trim())); 144 | map.put(p.key, p); 145 | if(strs[1].endsWith(String.valueOf(SEP))){ 146 | last = p; 147 | p.value = p.value + System.lineSeparator(); 148 | } 149 | } 150 | else{ 151 | lines.add(new Line(line)); 152 | } 153 | } 154 | else { 155 | if(strs.length != 2) 156 | continue; 157 | Pair pair = map.get(strs[0].trim()); 158 | if(pair != null && !strs[1].trim().startsWith(mark)){ 159 | pair.value = strs[1].trim(); 160 | if(strs[1].endsWith(String.valueOf(SEP))){ 161 | last = pair; 162 | pair.value += System.lineSeparator(); 163 | } 164 | } 165 | } 166 | } 167 | }catch(IOException e){ 168 | throw new RuntimeException(e); 169 | } 170 | } 171 | 172 | public void write(File file) { 173 | try { 174 | BufferedWriter writer = new BufferedWriter(new FileWriter(file, StandardCharsets.ISO_8859_1, false)); 175 | for (Line line: lines) { 176 | writer.write(line.toString()); 177 | writer.newLine(); 178 | writer.flush(); 179 | } 180 | } catch (IOException e) { 181 | throw new RuntimeException(e); 182 | } 183 | } 184 | } 185 | 186 | public static class Line{ 187 | String string; 188 | 189 | public Line(String line){ 190 | this.string = line; 191 | } 192 | 193 | @Override 194 | public String toString(){ 195 | return string; 196 | } 197 | } 198 | 199 | public static class Pair extends Line{ 200 | String key; 201 | String value; 202 | 203 | public Pair(String line, String key, String value){ 204 | super(line); 205 | this.key = key; 206 | this.value = value; 207 | } 208 | 209 | @Override 210 | public String toString(){ 211 | return key + " = " + value; 212 | } 213 | } 214 | 215 | private static String string2Unicode(String string) { 216 | StringBuilder unicode = new StringBuilder(); 217 | for (int i = 0; i < string.length(); i++) { 218 | // 取出每一个字符 219 | char c = string.charAt(i); 220 | if (c<0x20 || c>0x7E) { 221 | // 转换为unicode 222 | String tmp = Integer.toHexString(c).toUpperCase(); 223 | if (tmp.length() == 4) { 224 | unicode.append("\\u").append(tmp); 225 | } else if (tmp.length() == 3){ 226 | unicode.append("\\u0").append(tmp); 227 | } else if (tmp.length() == 2){ 228 | unicode.append("\\u00").append(tmp); 229 | } else { 230 | unicode.append("\\u000").append(tmp); 231 | } 232 | } else { 233 | unicode.append(c); 234 | } 235 | } 236 | return unicode.toString(); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /assets/bundles/bundle.properties: -------------------------------------------------------------------------------- 1 | tmi.mod.version = 2.8 2 | tmi.mod.updateDate = 2025 Jun 5 3 | 4 | recipes.open = TMI 5 | 6 | tmi.power-mark = Power 7 | tmi.heat-mark = Heat 8 | 9 | dialog.recipes.title = Too Many Items 10 | dialog.recipes.unselected = Select a content: 11 | dialog.recipes.contents = All Contents 12 | dialog.recipes.total = {0} contents in total, {1} folded... 13 | dialog.recipes.pages = Page {0}/{1} 14 | dialog.recipes.jump_a = Page 15 | dialog.recipes.jump_b = /{0} 16 | dialog.recipes.currSelected = Selected: 17 | dialog.recipes.locked = Locked 18 | dialog.recipes.type_filter = Type Filter 19 | dialog.recipes.no_recipe = No Recipe! 20 | dialog.recipes.no_usage = No Usage! 21 | dialog.recipes.designer = Schematic Designer 22 | 23 | dialog.calculator.noPage = No file opened 24 | dialog.calculator.openPage = [accent]>[lightgray] Click the Tab bar above to create or open a design\n[accent]>[lightgray] New a page through \u201CFiles->New\u201D\n[accent]>[lightgray] Load a existed shd file through \u201CFiles->Open\u201D 25 | dialog.calculator.removeArea = Drag here to remove the card 26 | dialog.calculator.recipeMulti = Amount: {0}x 27 | dialog.calculator.recipeEff = Efficiency: {0}% 28 | dialog.calculator.config = Configure 29 | dialog.calculator.noFileOpened = no file was opened 30 | dialog.calculator.openedFile = opened design graph files 31 | dialog.calculator.recentFile = recent opened files 32 | dialog.calculator.fileOpened = The file has been opened. 33 | dialog.calculator.editLock = Edit Locked 34 | dialog.calculator.addRecipe = Add Card 35 | dialog.calculator.removeLinker = Remove Link 36 | dialog.calculator.addInput = Add Input 37 | dialog.calculator.addInputAs = Add Input of Selected Content 38 | dialog.calculator.addOutput = Add Output 39 | dialog.calculator.addOutputAs = Add Output of Selected Content 40 | dialog.calculator.noItems = No IO Item Type 41 | dialog.calculator.addItem = Add Items 42 | dialog.calculator.addGlobalOutput = Add global outputs 43 | dialog.calculator.addGlobalInput = Add global inputs 44 | dialog.calculator.clickFocus = Click to focus on the card 45 | dialog.calculator.setAmount = Set Amount 46 | dialog.calculator.input = Outside Input 47 | dialog.calculator.output = Target Output 48 | dialog.calculator.add = Add Cards 49 | dialog.calculator.standard = Format Card Positions 50 | dialog.calculator.switchToComp = Switch to complete card 51 | dialog.calculator.switchToSimple = Switch to simplified card 52 | dialog.calculator.align = Card Aligning Mode 53 | dialog.calculator.selecting = Filter Mode 54 | dialog.calculator.exportIMG = Export as image 55 | dialog.calculator.delete = Remove Mode 56 | dialog.calculator.lock = Lock Edition 57 | dialog.calculator.foldIconConfig = Set folded Icon 58 | dialog.calculator.export = Export As Image... 59 | dialog.calculator.balance = Balance 60 | dialog.calculator.sizeAlign = Align Card Size 61 | dialog.calculator.unAlignSize = Unalign Card Size 62 | dialog.calculator.targetRec = Balance Target 63 | dialog.calculator.productions = Production 64 | dialog.calculator.currentAmount = Current Amount 65 | dialog.calculator.proportionAssign = Input Ratio Distribution 66 | dialog.calculator.materials = Input Material 67 | dialog.calculator.expectAmount = Expected AMount 68 | dialog.calculator.actualAmount = Actual AMount 69 | dialog.calculator.configured = Configure 70 | dialog.calculator.status = Status 71 | dialog.calculator.statistic = statistics 72 | dialog.calculator.statInputs = total inputs 73 | dialog.calculator.statOutputs = total outputs 74 | dialog.calculator.statMissing = total missing 75 | dialog.calculator.statRedundant = total redundant 76 | dialog.calculator.statBlocks = blocks 77 | dialog.calculator.globalIOs = global inputs/outputs 78 | dialog.calculator.statMaterials = build requirements 79 | dialog.calculator.assigned = {0} [gray](Passed) 80 | dialog.calculator.expectedMultiple = Excepted Recipe Multiplier:{0} 81 | dialog.calculator.currentMul = Current Multiplier: {0}x {1} 82 | dialog.calculator.effScale = Efficiency Scale: 83 | dialog.calculator.exportPrev = Image Size: {0}x{1}px ({2}%) 84 | dialog.calculator.exportBoundX = Padding Width: 85 | dialog.calculator.exportBoundY = Padding Height: 86 | dialog.calculator.exportScale = Scale: 87 | dialog.calculator.exportFile = Select Export File: 88 | dialog.calculator.exportSuccess = Succeed to export 89 | dialog.calculator.exportFailed = Fail to export 90 | dialog.calculator.foldedCards = folded cards 91 | dialog.calculator.mainView = main view 92 | dialog.calculator.foldColor = Folded Color : 93 | dialog.calculator.foldBackColor = Background Color: 94 | dialog.calculator.foldIconColor = Folded Icon Color 95 | dialog.calculator.foldPane = fold pane 96 | dialog.calculator.exportSide = fold pane position 97 | dialog.calculator.foldedCardPadding = fold cards padding 98 | dialog.calculator.foldedCardScale = fold cards scaling 99 | 100 | calculator.config.multiple = Recipe Multiplier 101 | calculator.config.efficiencyScl = Boost: 102 | calculator.config.optionalMats = Optional Input: 103 | calculator.config.attributes = Environment: 104 | calculator.config.selectOptionals = Select Optional Input 105 | calculator.config.selectAttributes = Select Environment Boost 106 | calculator.config.attrTip = Note: The sum of environment item should not be larger than reality. 107 | calculator.config.noOptionals = No Optional Input 108 | calculator.config.noAttributes = No Environment Boost 109 | 110 | calculator.tabs.file = Files 111 | calculator.tabs.edit = Edit 112 | calculator.tabs.analyze = Analyze 113 | calculator.tabs.view = View 114 | calculator.tabs.help = Help 115 | 116 | dialog.recipes.mode_usage = Usage 117 | dialog.recipes.mode_recipe = Recipe 118 | dialog.recipes.mode_factory = Factory 119 | 120 | misc.cancel = Cancel 121 | misc.close = Close 122 | misc.ensure = OK 123 | misc.select = Select 124 | misc.search = Search 125 | misc.save = Save 126 | misc.undo = Undo 127 | misc.redo = Redo 128 | misc.fold = Fold 129 | misc.unfold = Unfold 130 | misc.allSelect = Select All 131 | misc.alignSize = Align Card Size 132 | misc.unAlignSize = Unalign Card Size 133 | misc.standardize = Format Card Positions 134 | misc.average = Distribute Equally 135 | misc.saveAs = Save as 136 | misc.saveAll = Save All 137 | misc.unsaved = File not saved 138 | misc.someUnsaved = Some files not saved 139 | misc.ensureClose = Unsaved files will lose. Are you sure to quit? 140 | misc.saveAndClose = Save And Quit 141 | misc.addCard = Add Card 142 | misc.addRecipeCard = Add Recipe Card 143 | misc.addOutputCard = Add Output Point 144 | misc.addInputCard = Add Input Point 145 | misc.new = New 146 | misc.open = Open 147 | misc.export = Export 148 | misc.settings = Setting 149 | misc.exportImg = Export as Image 150 | misc.exportText = Export as Text 151 | misc.exportStat = Export as Statistics 152 | misc.remove = Remove 153 | misc.copy = Copy 154 | misc.clip = clip 155 | misc.paste = Paste 156 | misc.file = Files 157 | misc.edit = Edit 158 | misc.help = Help 159 | misc.about = About 160 | misc.stat = Statistics 161 | misc.simulate = Stimulate 162 | misc.buildSteer = Building Guide 163 | misc.closeAllPage = Close All Pages 164 | misc.closeOtherPage = Close Other Pages 165 | misc.closeAllSaved = Close Saved Pages 166 | misc.resetView = Reset View Position 167 | misc.balance = Balance Number 168 | misc.unset = Unset 169 | misc.provided = Provided 170 | misc.assignInvalid = Sum of ratios is not 1 171 | misc.order = Ascend 172 | misc.reverse = Descend 173 | misc.defaultSort = Default 174 | misc.nameSort = Sort by names 175 | misc.modSort = Sort by mods 176 | misc.typeSort = Sort by types 177 | misc.factory = Crafting 178 | misc.collecting = Excavate 179 | misc.building = Building 180 | misc.generator = Generation 181 | misc.optional = Optional Boost 182 | misc.amount = Amount: {0} 183 | misc.invalid = Invalid 184 | misc.noInput = No Input 185 | misc.balanced = Balanced 186 | misc.empty = 187 | misc.pressAnyKeys = Press Enter to confirm 188 | misc.requireInput = [gray]No Input[] 189 | misc.designerHelp = Designer Help 190 | 191 | category.tmi.name = TooManyItems 192 | keybind.tmi_hot_key.name = TMI hot key 193 | keybind.undo.name = Undo 194 | keybind.redo.name = Redo 195 | 196 | setting.tmi_button.name = TMI Floating Bottom 197 | setting.tmi_items_pane.name = TMI Items Scrolling Layout 198 | setting.tmi_items_pane.description = Displays items as scrolling lists. (May cause performance problems) 199 | setting.tmi_enable_preview.name = Enable TMI Preview Function (Restart Require) 200 | setting.tmi_enable_preview.description = Are you sure to enable uncompleted preview functions? 201 | setting.tmi_gridSize.name = Designer Grid Size 202 | setting.tmi_gridSize.description = The size of the grid of Designer\u2019s background 203 | -------------------------------------------------------------------------------- /assets/git/binding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/git/binding.png -------------------------------------------------------------------------------- /assets/git/code_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/git/code_structure.png -------------------------------------------------------------------------------- /assets/git/hotkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/git/hotkey.png -------------------------------------------------------------------------------- /assets/git/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/git/preview.png -------------------------------------------------------------------------------- /assets/git/schematic_designer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/git/schematic_designer.png -------------------------------------------------------------------------------- /assets/git/tmibutton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/git/tmibutton.png -------------------------------------------------------------------------------- /assets/sprites/ui/a_z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/a_z.png -------------------------------------------------------------------------------- /assets/sprites/ui/balance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/balance.png -------------------------------------------------------------------------------- /assets/sprites/ui/clip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/clip.png -------------------------------------------------------------------------------- /assets/sprites/ui/inbalance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/inbalance.png -------------------------------------------------------------------------------- /assets/sprites/ui/panner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/panner.png -------------------------------------------------------------------------------- /assets/sprites/ui/side_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/side_bottom.png -------------------------------------------------------------------------------- /assets/sprites/ui/side_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/side_left.png -------------------------------------------------------------------------------- /assets/sprites/ui/side_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/side_right.png -------------------------------------------------------------------------------- /assets/sprites/ui/side_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/side_top.png -------------------------------------------------------------------------------- /assets/sprites/ui/time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/time.png -------------------------------------------------------------------------------- /assets/sprites/ui/tmi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/tmi.png -------------------------------------------------------------------------------- /assets/sprites/ui/ui-bottom-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/ui-bottom-left.png -------------------------------------------------------------------------------- /assets/sprites/ui/ui-bottom-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/ui-bottom-right.png -------------------------------------------------------------------------------- /assets/sprites/ui/ui-bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/ui-bottom.png -------------------------------------------------------------------------------- /assets/sprites/ui/ui-center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/ui-center.png -------------------------------------------------------------------------------- /assets/sprites/ui/ui-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/ui-left.png -------------------------------------------------------------------------------- /assets/sprites/ui/ui-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/ui-right.png -------------------------------------------------------------------------------- /assets/sprites/ui/ui-top-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/ui-top-left.png -------------------------------------------------------------------------------- /assets/sprites/ui/ui-top-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/ui-top-right.png -------------------------------------------------------------------------------- /assets/sprites/ui/ui-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/assets/sprites/ui/ui-top.png -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | 2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 3 | import java.io.FileOutputStream 4 | import java.io.InputStreamReader 5 | import java.io.StringReader 6 | import java.util.jar.JarEntry 7 | import java.util.jar.JarOutputStream 8 | 9 | val mindustryVersion = "v149" 10 | val arcVersion = "v149" 11 | 12 | val modOutputDir = properties["modOutputDir"] as? String 13 | 14 | val sdkRoot: String? = System.getenv("ANDROID_HOME") 15 | 16 | val buildDir = layout.buildDirectory.get() 17 | val projectName = project.name 18 | 19 | plugins { 20 | kotlin("jvm") version "2.1.20" 21 | `maven-publish` 22 | } 23 | 24 | group = "com.github.EB-wilson" 25 | version = "2.8" 26 | 27 | run { "java SyncBundles.java $version".execute() } 28 | 29 | java { 30 | sourceCompatibility = JavaVersion.VERSION_1_8 31 | targetCompatibility = JavaVersion.VERSION_1_8 32 | } 33 | 34 | kotlin { 35 | jvmToolchain(21) 36 | 37 | compilerOptions { 38 | jvmTarget.set(JvmTarget.JVM_1_8) 39 | } 40 | } 41 | 42 | publishing { 43 | publications { 44 | create("maven") { 45 | from(components["java"]) 46 | 47 | groupId = "com.github.EB-wilson" 48 | artifactId = "TooManyItems" 49 | version = "${project.version}" 50 | } 51 | } 52 | } 53 | 54 | repositories { 55 | mavenLocal() 56 | mavenCentral() 57 | maven ("https://maven.xpdustry.com/mindustry") 58 | maven { url = uri("https://raw.githubusercontent.com/Zelaux/MindustryRepo/master/repository") } 59 | maven { url = uri("https://www.jitpack.io") } 60 | } 61 | 62 | dependencies { 63 | compileOnly("com.github.Anuken.Arc:arc-core:$arcVersion") 64 | compileOnly("com.github.Anuken.Mindustry:core:$mindustryVersion") 65 | 66 | implementation("com.github.EB-wilson.UniverseCore:markdown:2.1.1") 67 | 68 | implementation(kotlin("stdlib-jdk8")) 69 | } 70 | 71 | tasks { 72 | jar{ 73 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 74 | archiveFileName = "${project.name}-desktop.jar" 75 | 76 | from(rootDir) { 77 | include("mod.hjson") 78 | include("icon.png") 79 | include("contributors.hjson") 80 | } 81 | 82 | from("assets/") { 83 | include("**") 84 | exclude("git") 85 | } 86 | 87 | from(configurations.getByName("runtimeClasspath").map { if (it.isDirectory) it else zipTree(it) }) 88 | } 89 | 90 | register("jarAndroid") { 91 | dependsOn("jar") 92 | 93 | doLast { 94 | try { 95 | if (sdkRoot == null) throw GradleException("No valid Android SDK found. Ensure that ANDROID_HOME is set to your Android SDK directory."); 96 | 97 | val platformRoot = File("$sdkRoot/platforms/").listFiles() 98 | ?.sorted() 99 | ?.reversed() 100 | ?.find { f -> File (f, "android.jar").exists() } 101 | 102 | if (platformRoot == null) throw GradleException("No android.jar found. Ensure that you have an Android platform installed.") 103 | 104 | //collect dependencies needed for desugaring 105 | val dependencies = ( 106 | configurations.compileClasspath.get().files + 107 | configurations.runtimeClasspath.get().files + 108 | setOf(File(platformRoot, "android.jar")) 109 | ).joinToString(" ") { "--classpath $it" } 110 | 111 | //dex and desugar files - this requires d8 in your PATH 112 | "d8 $dependencies --min-api 14 --output ${project.name}-android.jar ${project.name}-desktop.jar" 113 | .execute(File("$buildDir/libs")) 114 | } 115 | catch (e: Throwable) { 116 | if (e is Error){ 117 | println(e.message) 118 | return@doLast 119 | } 120 | 121 | println("[WARNING] d8 tool or platform tools was not found, if you was installed android SDK, please check your environment variable") 122 | 123 | delete( 124 | files("${buildDir}/libs/${project.name}-android.jar") 125 | ) 126 | 127 | val out = JarOutputStream(FileOutputStream("${buildDir}/libs/${project.name}-android.jar")) 128 | out.putNextEntry(JarEntry("non-androidMod.txt")) 129 | val reader = StringReader( 130 | "this mod is don't have classes.dex for android, please consider recompile with a SDK or run this mod on desktop only" 131 | ) 132 | 133 | var r = reader.read() 134 | while (r != -1) { 135 | out.write(r) 136 | out.flush() 137 | r = reader.read() 138 | } 139 | out.close() 140 | } 141 | } 142 | } 143 | 144 | register("deploy", Jar::class) { 145 | dependsOn("jarAndroid") 146 | 147 | from ( 148 | zipTree("${buildDir}/libs/${project.name}-desktop.jar"), 149 | zipTree("${buildDir}/libs/${project.name}-android.jar") 150 | ) 151 | 152 | doLast { 153 | if (!modOutputDir.isNullOrEmpty()) { 154 | copy { 155 | into("$modOutputDir/") 156 | from("${buildDir}/libs/${project.name}-${version}.jar") 157 | } 158 | } 159 | } 160 | } 161 | 162 | register("deployDesktop", Jar::class) { 163 | dependsOn("jar") 164 | archiveFileName = "${project.name}.jar" 165 | 166 | from (zipTree("${buildDir}/libs/${project.name}-desktop.jar")) 167 | 168 | doLast { 169 | if (!modOutputDir.isNullOrEmpty()) { 170 | copy { 171 | into("$modOutputDir/") 172 | from("${buildDir}/libs/${project.name}.jar") 173 | } 174 | } 175 | } 176 | } 177 | 178 | register("debugMod", JavaExec::class) { 179 | dependsOn("classes") 180 | dependsOn("deployDesktop") 181 | 182 | mainClass = "-jar" 183 | args = listOf( 184 | project.properties["debugGamePath"] as? String?:"", 185 | "-debug" 186 | ) 187 | } 188 | } 189 | 190 | fun String.execute(path: File? = null, vararg args: Any?): Process{ 191 | val cmd = split(Regex("\\s+")) 192 | .toMutableList() 193 | .apply { addAll(args.map { it?.toString()?:"null" }) } 194 | .toTypedArray() 195 | val process = ProcessBuilder(*cmd) 196 | .directory(path?:rootDir) 197 | .redirectOutput(ProcessBuilder.Redirect.INHERIT) 198 | .redirectError(ProcessBuilder.Redirect.INHERIT) 199 | .start() 200 | 201 | if (process.waitFor() != 0) throw Error(InputStreamReader(process.errorStream).readText()) 202 | 203 | return process 204 | } 205 | 206 | class Error(str: String): RuntimeException(str) 207 | -------------------------------------------------------------------------------- /bundleInfo.properties: -------------------------------------------------------------------------------- 1 | bundlesDir = assets/bundles/ 2 | 3 | modName = tmi 4 | source = zh_CN 5 | 6 | targetLocales = [\ 7 | en_US = (wait to translate)\ 8 | zh_TW = (\u5F85\u7FFB\u8B6F)\ 9 | ru = (\u043D\u0443\u0436\u0435\u043D \u043F\u0435\u0440\u0435\u0432\u043E\u0434)\ 10 | ja = (\u7FFB\u8A33\u5F85\u3061)\ 11 | de_DE = (\u00DCbersetzung w\u00FCnschen)\ 12 | ] 13 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | 2 | #mod build to directory in this 3 | modOutputDir = 4 | debugGamePath = 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${varSET.getWrap()}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EB-wilson/TooManyItems/eddfe0f90b7f8144e15d8764af2a36af79998eaa/icon.png -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 3 | install: | 4 | chmod 777 ./gradlew 5 | ./gradlew publishToMavenLocal -------------------------------------------------------------------------------- /mod.hjson: -------------------------------------------------------------------------------- 1 | { 2 | name: "tmi", 3 | displayName: "Too Many Items", 4 | author: "EBwilson", 5 | main: "tmi.TooManyItems", 6 | description: "An auxiliary mod that provides you with quick retrieval of production and collection guidance information for materials, units, buildings, minerals, etc., so that you are no longer overwhelmed by installing too many mods.\n\n---\n\n一个辅助mod,为您提供快速检索材料,单位,建筑和矿物等的生产与采集引导信息,让您不再因为安装了太多mod而不知所措", 7 | version: "2.8", 8 | minGameVersion: 147, 9 | hidden: true 10 | } 11 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "TooManyItems" 2 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/RecipeEntry.kt: -------------------------------------------------------------------------------- 1 | package tmi 2 | 3 | /**三方mod的被动加载入口接口,为其他mod注册TMI配方提供易用的配方加载器。 4 | * 5 | * 由于被动加载的特性,调用方可以在其mod中开辟一个类似于[mindustry.mod.Mod]的入口分支, 6 | * 这个分支当中可以安全的依赖TooManyItems的代码,而无需担心在没有安装TMI的情况下造成[ClassNotFoundException] 7 | * 8 | * 要使用这个接口,您只需要像mod表示主类类路径那样,在mod.json(.hjson)当中添加:`"recipeEntry": xxx`,并将xxx填写为指向一个实现该接口的类路径即可 */ 9 | interface RecipeEntry { 10 | fun init() 11 | fun afterInit(){ /* default no action */ } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/RecipeEntryPoint.kt: -------------------------------------------------------------------------------- 1 | package tmi 2 | 3 | import kotlin.reflect.KClass 4 | 5 | /**入口点绑定标记,注释在mod主类型上以标记此mod的TMI配方适配器的入口类型 6 | * 7 | * @param value 配方适配器类型,填写你的适配器主类即可*/ 8 | @Target(AnnotationTarget.TYPE) 9 | @Retention(AnnotationRetention.RUNTIME) 10 | annotation class RecipeEntryPoint( 11 | val value: KClass 12 | ) -------------------------------------------------------------------------------- /src/main/kotlin/tmi/TooManyItems.kt: -------------------------------------------------------------------------------- 1 | package tmi 2 | 3 | import arc.Events 4 | import arc.func.* 5 | import arc.struct.IntMap 6 | import arc.struct.ObjectFloatMap 7 | import arc.struct.ObjectIntMap 8 | import arc.struct.ObjectMap 9 | import arc.util.Time 10 | import arc.util.io.Writes 11 | import mindustry.Vars 12 | import mindustry.game.EventType.ClientLoadEvent 13 | import mindustry.mod.Mod 14 | import tmi.recipe.RecipeItemManager 15 | import tmi.recipe.RecipesManager 16 | import tmi.recipe.parser.* 17 | import tmi.ui.Cursor 18 | import tmi.ui.EntryAssigner 19 | import tmi.ui.TmiUI 20 | import tmi.util.KeyBinds 21 | 22 | class TooManyItems : Mod() { 23 | companion object { 24 | @JvmField 25 | var recipesManager: RecipesManager = RecipesManager() 26 | @JvmField 27 | var itemsManager: RecipeItemManager = RecipeItemManager() 28 | @JvmField 29 | var api: ModAPI = ModAPI() 30 | @JvmField 31 | val binds = KeyBinds() 32 | } 33 | 34 | init { 35 | ConsumerParser.registerVanillaConsumeParser() 36 | registerDefaultParser() 37 | 38 | Events.on(ClientLoadEvent::class.java) { 39 | Time.runTask(0f) { 40 | EntryAssigner.assign() 41 | Vars.ui.settings.game.checkPref("tmi_button", true) 42 | Vars.ui.settings.game.checkPref("tmi_items_pane", false) 43 | Vars.ui.settings.graphics.sliderPref("tmi_gridSize", 150, 50, 300, 10) { i -> i.toString() } 44 | api.afterInit() 45 | } 46 | } 47 | } 48 | 49 | private fun registerDefaultParser() { 50 | //几乎所有的原版游戏工厂方块的分析工具 51 | recipesManager.registerParser(GenericCrafterParser()) 52 | recipesManager.registerParser(UnitFactoryParser()) 53 | recipesManager.registerParser(ReconstructorParser()) 54 | recipesManager.registerParser(UnitAssemblerParser()) 55 | recipesManager.registerParser(ConstructorParser()) 56 | recipesManager.registerParser(PumpParser()) 57 | recipesManager.registerParser(SolidPumpParser()) 58 | recipesManager.registerParser(FrackerParser()) 59 | recipesManager.registerParser(DrillParser()) 60 | recipesManager.registerParser(BeamDrillParser()) 61 | recipesManager.registerParser(SeparatorParser()) 62 | recipesManager.registerParser(GeneratorParser()) 63 | recipesManager.registerParser(ConsGeneratorParser()) 64 | recipesManager.registerParser(HeatGeneratorParser()) 65 | recipesManager.registerParser(ThermalGeneratorParser()) 66 | recipesManager.registerParser(VariableReactorParser()) 67 | recipesManager.registerParser(HeatCrafterParser()) 68 | recipesManager.registerParser(HeatProducerParser()) 69 | recipesManager.registerParser(AttributeCrafterParser()) 70 | recipesManager.registerParser(WallCrafterParser()) 71 | } 72 | 73 | override fun init() { 74 | Cursor.init() 75 | binds.load() 76 | api.init() 77 | 78 | recipesManager.init() 79 | 80 | TmiUI.init() 81 | } 82 | } 83 | 84 | operator fun ObjectIntMap.set(key: K, value: Int) = put(key, value) 85 | operator fun ObjectFloatMap.set(key: K, value: Float) = put(key, value) 86 | 87 | operator fun IntMap.set(key: Int, value: V): V = put(key, value) 88 | 89 | operator fun ObjectMap.set(key: K, value: V): V = put(key, value) 90 | 91 | operator fun

Cons

.invoke(p: P) = get(p) 92 | operator fun Cons2.invoke(p1: P1, p2: P2) = get(p1, p2) 93 | operator fun Cons3.invoke(p1: P1, p2: P2, p3: P3) = get(p1, p2, p3) 94 | operator fun Cons4.invoke(p1: P1, p2: P2, p3: P3, p4: P4) = get(p1, p2, p3, p4) 95 | operator fun Func.invoke(p: P): R = get(p) 96 | operator fun Func2.invoke(p1: P1, p2: P2): R = get(p1, p2) 97 | operator fun Func3.invoke(p1: P1, p2: P2, p3: P3): R = get(p1, p2, p3) 98 | operator fun Prov.invoke(): R = get() 99 | operator fun ConsT.invoke(p: P) = get(p) 100 | 101 | fun Writes.b(vararg bytes: Int) = bytes.forEach { b(it) } 102 | fun Writes.s(vararg shorts: Int) = shorts.forEach { s(it) } 103 | fun Writes.i(vararg ints: Int) = ints.forEach { i(it) } 104 | fun Writes.l(vararg longs: Long) = longs.forEach { l(it) } 105 | fun Writes.f(vararg floats: Float) = floats.forEach { f(it) } 106 | fun Writes.d(vararg bytes: Double) = bytes.forEach { d(it) } 107 | fun Writes.str(vararg strings: String) = strings.forEach { str(it) } 108 | fun Writes.bool(vararg bools: Boolean) = bools.forEach { bool(it) } 109 | 110 | inline fun ObjectMap.forEach(block: (K, V) -> Unit){ 111 | for (e in this){ 112 | block(e.key, e.value) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/AmountFormatter.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe 2 | 3 | import arc.math.Mathf 4 | import arc.util.Strings 5 | import mindustry.core.UI 6 | import tmi.util.Utils 7 | 8 | fun interface AmountFormatter { 9 | companion object{ 10 | @JvmStatic 11 | fun emptyFormatter() = AmountFormatter { "" } 12 | 13 | @JvmStatic 14 | @JvmOverloads 15 | fun floatFormatter(multiplier: Float = 1f) = AmountFormatter{ f -> 16 | if (f*multiplier > 1000) UI.formatAmount(Mathf.round(f*multiplier).toLong()) 17 | else Strings.autoFixed(f*multiplier, 1) 18 | } 19 | 20 | @JvmStatic 21 | @JvmOverloads 22 | fun integerFormatter(multiplier: Float = 1f) = AmountFormatter{ f -> 23 | if (f*multiplier > 1000) UI.formatAmount(Mathf.round(f*multiplier).toLong()) 24 | else Mathf.round(f*multiplier).toString() 25 | } 26 | 27 | @JvmStatic 28 | fun unitTimedFormatter() = AmountFormatter{ f -> 29 | val (value, unit) = Utils.unitTimed(f) 30 | 31 | (if (value > 1000) UI.formatAmount((value).toLong()) 32 | else Strings.autoFixed(value, 1)) + unit 33 | } 34 | 35 | @Deprecated( 36 | message = "standardized function name to unitTimedFormatter", 37 | replaceWith = ReplaceWith("unitTimedFormatter()") 38 | ) 39 | @JvmStatic 40 | fun persecFormatter() = AmountFormatter{ f -> 41 | (if (f*60 > 1000) UI.formatAmount((f*60).toLong()) 42 | else Strings.autoFixed(f*60, 1)) + "[gray]/sec" 43 | } 44 | } 45 | 46 | fun format(f: Float): String 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/EnvParameter.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe 2 | 3 | import arc.func.Cons2 4 | import arc.struct.ObjectFloatMap 5 | import arc.util.io.Reads 6 | import arc.util.io.Writes 7 | import tmi.TooManyItems 8 | import tmi.recipe.types.HeatMark 9 | import tmi.recipe.types.PowerMark 10 | import tmi.recipe.types.RecipeItem 11 | 12 | class EnvParameter { 13 | private val inputs = ObjectFloatMap>() 14 | private val attributes = ObjectFloatMap>() 15 | 16 | fun getInputs(b: RecipeItem<*>?): Float { 17 | return inputs[b, 0f] 18 | } 19 | 20 | fun getAttribute(b: RecipeItem<*>?): Float { 21 | return attributes[b, 0f] 22 | } 23 | 24 | fun add(item: RecipeItemStack<*>): EnvParameter { 25 | return add(item.item, item.amount, item.isAttribute) 26 | } 27 | 28 | fun add(item: RecipeItem<*>?, amount: Float, isAttribute: Boolean): EnvParameter { 29 | if (isAttribute) { 30 | attributes.increment(item, 0f, amount) 31 | } 32 | else inputs.increment(item, 0f, amount) 33 | 34 | return this 35 | } 36 | 37 | @JvmOverloads 38 | fun set(other: EnvParameter, over: Boolean = false): EnvParameter { 39 | if (over) clear() 40 | other.attributes.each { e: ObjectFloatMap.Entry?> -> add(e.key, e.value, true) } 41 | other.inputs.each { e: ObjectFloatMap.Entry?> -> add(e.key, e.value, false) } 42 | return this 43 | } 44 | 45 | @JvmOverloads 46 | fun setInputs(other: EnvParameter, over: Boolean = false): EnvParameter { 47 | if (over) clearInputs() 48 | other.inputs.each { e: ObjectFloatMap.Entry?> -> add(e.key, e.value, false) } 49 | return this 50 | } 51 | 52 | @JvmOverloads 53 | fun setAttributes(other: EnvParameter, over: Boolean = false): EnvParameter { 54 | if (over) clearAttr() 55 | other.attributes.each { e: ObjectFloatMap.Entry?> -> add(e.key, e.value, true) } 56 | return this 57 | } 58 | 59 | fun resetInput(item: RecipeItem<*>?): EnvParameter { 60 | inputs.remove(item, 0f) 61 | return this 62 | } 63 | 64 | fun resetAttr(item: RecipeItem<*>?): EnvParameter { 65 | attributes.remove(item, 0f) 66 | return this 67 | } 68 | 69 | fun clearInputs(): EnvParameter { 70 | inputs.clear() 71 | return this 72 | } 73 | 74 | fun clearAttr(): EnvParameter { 75 | attributes.clear() 76 | return this 77 | } 78 | 79 | fun clear(): EnvParameter { 80 | clearInputs() 81 | clearAttr() 82 | return this 83 | } 84 | 85 | fun applyFullRecipe(recipe: Recipe, fillOptional: Boolean, applyAttribute: Boolean, multiplier: Float): EnvParameter { 86 | for (stack in recipe.materials.values()) { 87 | if (!fillOptional && stack.optionalCons) continue 88 | if (!applyAttribute && stack.isAttribute) continue 89 | 90 | inputs.put(stack.item, stack.amount*multiplier) 91 | } 92 | 93 | return this 94 | } 95 | 96 | fun addPower(power: Float): EnvParameter { 97 | add(PowerMark, power + inputs[PowerMark, 0f], false) 98 | return this 99 | } 100 | 101 | fun addHeat(heat: Float): EnvParameter { 102 | add(HeatMark, heat + inputs[HeatMark, 0f], false) 103 | return this 104 | } 105 | 106 | fun hasInput(): Boolean { 107 | return !inputs.isEmpty 108 | } 109 | 110 | fun hasAttrs(): Boolean { 111 | return !attributes.isEmpty 112 | } 113 | 114 | fun eachInputs(cons: Cons2, Float>) { 115 | inputs.each { e: ObjectFloatMap.Entry> -> cons[e.key, e.value] } 116 | } 117 | 118 | fun eachAttribute(cons: Cons2, Float>) { 119 | attributes.each { e: ObjectFloatMap.Entry> -> cons[e.key, e.value] } 120 | } 121 | 122 | fun copy(): EnvParameter{ 123 | val copy = EnvParameter() 124 | copy.inputs.putAll(inputs) 125 | copy.attributes.putAll(attributes) 126 | return copy 127 | } 128 | 129 | fun write(write: Writes) { 130 | write.i(inputs.size) 131 | write.i(attributes.size) 132 | 133 | inputs.each { 134 | write.str(it.key.name) 135 | write.f(it.value) 136 | } 137 | 138 | attributes.each { 139 | write.str(it.key.name) 140 | write.f(it.value) 141 | } 142 | } 143 | 144 | fun read(read: Reads){ 145 | val ins = read.i() 146 | val attrs = read.i() 147 | 148 | for (i in 0 until ins) { 149 | inputs.put(TooManyItems.itemsManager.getByName(read.str()), read.f()) 150 | } 151 | 152 | for (i in 0 until attrs) { 153 | attributes.put(TooManyItems.itemsManager.getByName(read.str()), read.f()) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/RecipeItemManager.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe 2 | 3 | import arc.Core 4 | import arc.func.Boolf 5 | import arc.func.Func 6 | import arc.graphics.g2d.TextureRegion 7 | import arc.struct.ObjectIntMap 8 | import arc.struct.ObjectMap 9 | import arc.struct.OrderedMap 10 | import arc.struct.Seq 11 | import mindustry.Vars 12 | import mindustry.ctype.ContentType 13 | import mindustry.ctype.UnlockableContent 14 | import tmi.invoke 15 | import tmi.recipe.types.RecipeItem 16 | import tmi.recipe.types.SingleItemMark 17 | import tmi.set 18 | 19 | class RecipeItemManager { 20 | private val recipeItems = ObjectMap>() 21 | private val itemNameMap = ObjectMap>() 22 | 23 | fun > addItemWrap(item: T, recipeItem: R): R { 24 | recipeItems.put(item, recipeItem) 25 | itemNameMap.put(recipeItem.name, recipeItem) 26 | 27 | return recipeItem 28 | } 29 | 30 | @Suppress("UNCHECKED_CAST") 31 | fun getItem(item: T): RecipeItem { 32 | return recipeItems.get(item) { 33 | for (entry in wrapper) { 34 | if (entry.key[item]) { 35 | val res = (entry.value as Func>)(item) 36 | itemNameMap.put(res.name, res) 37 | return@get res 38 | } 39 | } 40 | ERROR 41 | } as RecipeItem 42 | } 43 | 44 | @Suppress("UNCHECKED_CAST") 45 | fun getByName(name: String): RecipeItem { 46 | return itemNameMap[name, ERROR] as RecipeItem 47 | } 48 | 49 | val list: Seq> 50 | get() = recipeItems.values().toSeq().sort() 51 | 52 | private class RecipeUnlockableContent(item: UnlockableContent) : RecipeItem(item) { 53 | override val ordinal = item.id.toInt() 54 | override val typeOrdinal = mirror[item.contentType, item.contentType.ordinal] 55 | override val typeID: Int = item.contentType.ordinal 56 | override val name: String = item.name 57 | override val localizedName: String = item.localizedName 58 | override val icon: TextureRegion = item.uiIcon?:throw IllegalStateException("Item $name no icon") 59 | override val hidden = false 60 | override val locked = !item.unlockedNow() 61 | override val hasDetails = true 62 | 63 | override fun displayDetails() = Vars.ui.content.show(item) 64 | 65 | companion object { 66 | private val mirror = ObjectIntMap() 67 | 68 | init { 69 | mirror.put(ContentType.item, 0) 70 | mirror.put(ContentType.liquid, 1) 71 | mirror.put(ContentType.block, 2) 72 | mirror.put(ContentType.unit, 3) 73 | } 74 | } 75 | } 76 | 77 | companion object { 78 | private val ERROR = object : SingleItemMark("") { 79 | override val icon get() = Core.atlas.find("error") 80 | override val hidden = true 81 | } 82 | 83 | val wrapper = OrderedMap, Func<*, RecipeItem<*>>>() 84 | 85 | @Suppress("UNCHECKED_CAST") 86 | @JvmStatic 87 | fun registerWrapper(matcher: Boolf, factory: Func>) { 88 | wrapper[matcher] = factory as Func<*, RecipeItem<*>> 89 | } 90 | 91 | @Suppress("UNCHECKED_CAST") 92 | inline fun registerWrapper(factory: Func>) { 93 | wrapper[Boolf{ e: Any? -> e is T }] = factory as Func<*, RecipeItem<*>> 94 | } 95 | 96 | init { 97 | registerWrapper { RecipeUnlockableContent(it) } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/RecipeItemStack.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe 2 | 3 | import mindustry.ctype.UnlockableContent 4 | import tmi.recipe.AmountFormatter.Companion.emptyFormatter 5 | import tmi.recipe.AmountFormatter.Companion.floatFormatter 6 | import tmi.recipe.AmountFormatter.Companion.integerFormatter 7 | import tmi.recipe.AmountFormatter.Companion.persecFormatter 8 | import tmi.recipe.AmountFormatter.Companion.unitTimedFormatter 9 | import tmi.recipe.types.RecipeItem 10 | 11 | /**保存一个材料项目数据的结构类型,在[Recipe]中作数据记录对象使用 */ 12 | class RecipeItemStack( 13 | /**该条目表示的[UnlockableContent] */ 14 | val item: RecipeItem, 15 | /**条目附加的数量信息,这将被用作生产计算和显示数据的文本格式化 */ 16 | var amount: Float = 0f 17 | ) { 18 | /**条目数据显示的文本格式化函数,这里返回的文本将显示在条目上以表示数量信息 */ 19 | var amountFormat: AmountFormatter = AmountFormatter { "" } 20 | private set 21 | /**在按下TMI热键时显示的备选数目文本的格式化函数,例如每秒消耗的备选文本为单次配方工作的总消耗,而总消耗的备选文本为每秒消耗等*/ 22 | var alternativeFormat: AmountFormatter? = null 23 | private set 24 | /**此条目的效率系数,应当绑定为该条目在生产工作中可用时的最高效率倍率,以参与生产计算 */ 25 | var efficiency = 1f 26 | private set 27 | /**该条目是否为可选消耗项,应当与实际情况同步 */ 28 | var optionalCons = false 29 | private set 30 | /**该条目是否为属性项目,通常用于计算覆盖/关联的方块提供的属性增益 */ 31 | var isAttribute = false 32 | private set 33 | /**该条目是否是增幅项目,若为增幅项目则被单独计算boost倍率,该倍率将影响输入物数量计算并直接乘在最终效率上 */ 34 | var isBooster = false 35 | private set 36 | /**条目从属的属性组,一个属性组内的项目在工作效率计算时,会以最高的那一个作为计算结果。 37 | *

38 | * 属性组的划分按照提供的对象确定,任意时候当两个条目的属性组对象[Object.equals]为真时就会被视为从属于同一属性组。 39 | * 该字段默认空,为空时表示该条目不从属于任何属性组 */ 40 | var attributeGroup: Any? = null 41 | private set 42 | /**若为真,此消耗项的属性效率计算会按属性组的最大效率计算,否则会对属性效率求和 */ 43 | var maxAttribute = false 44 | private set 45 | 46 | /**获取此堆所保存的而物品类型*/ 47 | fun item(): RecipeItem = item 48 | /**获取经过格式化的表示数量的文本信息 */ 49 | fun getAmount() = amountFormat.format(amount) 50 | 51 | //Deprecated 52 | @Deprecated(message = "this is a unstandardized function", replaceWith = ReplaceWith("emptyFormat()"), level = DeprecationLevel.ERROR) 53 | fun setEmptyFormat() = emptyFormat() 54 | @Deprecated(message = "this is a unstandardized function", replaceWith = ReplaceWith("floatFormat()"), level = DeprecationLevel.ERROR) 55 | @JvmOverloads 56 | fun setFloatFormat(mul: Float = 1f) = floatFormat(mul) 57 | @Deprecated(message = "this is a unstandardized function", replaceWith = ReplaceWith("integerFormat()"), level = DeprecationLevel.ERROR) 58 | @JvmOverloads 59 | fun setIntegerFormat(mul: Float = 1f) = integerFormat(mul) 60 | @Deprecated(message = "this is a unstandardized function", replaceWith = ReplaceWith("persecFormat()"), level = DeprecationLevel.ERROR) 61 | fun setPersecFormat() = persecFormat() 62 | @Deprecated(message = "this is a unstandardized function", replaceWith = ReplaceWith("setAltFormat(format)"), level = DeprecationLevel.ERROR) 63 | fun setAltPersecFormat() = setFormat(persecFormatter()) 64 | 65 | //属性设置的工具方法 66 | fun setEff(efficiency: Float) = also { this.efficiency = efficiency } 67 | @JvmOverloads 68 | fun setOptional(optionalCons: Boolean = true) = also { this.optionalCons = optionalCons } 69 | @JvmOverloads 70 | fun setAttribute(isAttr: Boolean = true) = also { isAttribute = isAttr } 71 | @JvmOverloads 72 | fun setBooster(boost: Boolean = true) = also { isBooster = boost } 73 | @JvmOverloads 74 | fun setMaxAttr(isMax: Boolean = true) = also { maxAttribute = isMax } 75 | fun setAttribute(group: Any?) = also { attributeGroup = group } 76 | fun setFormat(format: AmountFormatter) = also { amountFormat = format } 77 | fun setAltFormat(format: AmountFormatter) = also { alternativeFormat = format } 78 | 79 | // utils 80 | fun emptyFormat() = also { setFormat(emptyFormatter()) } 81 | @JvmOverloads 82 | fun floatFormat(mul: Float = 1f) = also { setFormat(floatFormatter(mul)) } 83 | @JvmOverloads 84 | fun integerFormat(mul: Float = 1f) = also { setFormat(integerFormatter(mul)) } 85 | fun unitTimedFormat() = also { setFormat( unitTimedFormatter()) } 86 | @Deprecated( 87 | message = "standardized function name to unitTimedFormat", 88 | replaceWith = ReplaceWith("unitTimedFormat()") 89 | ) 90 | fun persecFormat() = also { setFormat(persecFormatter()) } 91 | 92 | fun copy() = RecipeItemStack(item, amount).also{ 93 | it.amountFormat = amountFormat 94 | it.alternativeFormat = alternativeFormat 95 | it.efficiency = efficiency 96 | it.optionalCons = optionalCons 97 | it.isAttribute = isAttribute 98 | it.isBooster = isBooster 99 | it.attributeGroup = attributeGroup 100 | it.maxAttribute = maxAttribute 101 | } 102 | 103 | override fun equals(other: Any?): Boolean { 104 | if (this === other) return true 105 | if (other == null || javaClass != other.javaClass) return false 106 | val stack = other as RecipeItemStack<*> 107 | return amount == stack.amount 108 | && isAttribute == stack.isAttribute 109 | && isBooster == stack.isBooster 110 | && maxAttribute == stack.maxAttribute 111 | && item == stack.item 112 | && attributeGroup == stack.attributeGroup 113 | } 114 | 115 | override fun hashCode(): Int { 116 | var result = item.hashCode() 117 | result = 31*result + amount.hashCode() 118 | result = 31*result + isAttribute.hashCode() 119 | result = 31*result + isBooster.hashCode() 120 | result = 31*result + (attributeGroup?.hashCode() ?: 0) 121 | result = 31*result + maxAttribute.hashCode() 122 | return result 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/RecipeParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe 2 | 3 | import arc.struct.Seq 4 | import mindustry.world.Block 5 | import tmi.TooManyItems 6 | import tmi.recipe.types.RecipeItem 7 | 8 | abstract class RecipeParser { 9 | /**互斥解析器的类型列表 */ 10 | var excludes = Seq>>() 11 | 12 | /**在处理前初始化该分析器 */ 13 | open fun init() {} 14 | 15 | /**该分析器对于参数给定的那个分析器是否是互斥的,该方法返回true会阻止另一个配方分析器分析此方块。 16 | * 17 | * 例如一个方块可以被两个分析器分析,但是该分析器与另一个互斥,那么那一个分析器将被跳过而不解释这一方块 */ 18 | fun exclude(parser: RecipeParser<*>): Boolean { 19 | return excludes.contains { e: Class> -> e.isAssignableFrom(parser.javaClass) } 20 | } 21 | 22 | /**给出的方块是否是此配方分析器工作的目标方块,若是,且没有另一个同样以此方块为目标的解析器与这个解析器[互斥][RecipeParser.exclude],则该解析器将被应用于该方块 */ 23 | abstract fun isTarget(content: Block): Boolean 24 | 25 | /**分析一个方块,将其可执行的所有配方以一个[Seq]的形式返回。生成的配方应当和设施在实际运转中的各数据保持一致。 */ 26 | abstract fun parse(content: T): Seq 27 | 28 | companion object { 29 | @JvmStatic 30 | protected fun T.getWrap(): RecipeItem { 31 | return wrapItem(this) 32 | } 33 | 34 | @JvmStatic 35 | protected fun wrapItem(item: I): RecipeItem { 36 | return TooManyItems.itemsManager.getItem(item) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/RecipeType.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe 2 | 3 | import arc.Core 4 | import arc.graphics.g2d.Draw 5 | import arc.graphics.g2d.Lines 6 | import arc.math.geom.Vec2 7 | import arc.scene.Group 8 | import arc.scene.ui.layout.Scl 9 | import arc.struct.Seq 10 | import arc.util.Time 11 | import tmi.recipe.types.BuildingRecipe 12 | import tmi.recipe.types.CollectingRecipe 13 | import tmi.recipe.types.FactoryRecipe 14 | import tmi.recipe.types.GeneratorRecipe 15 | import tmi.ui.RecipeNode 16 | import tmi.ui.RecipeView 17 | import tmi.ui.RecipeView.LineMeta 18 | import kotlin.reflect.KVisibility 19 | 20 | /**配方表类型,用于描述一个配方如何被显示或者计算等 */ 21 | abstract class RecipeType { 22 | companion object { 23 | val all = Seq() 24 | 25 | @JvmField 26 | val factory = FactoryRecipe() 27 | @JvmField 28 | val building = BuildingRecipe() 29 | @JvmField 30 | val collecting = CollectingRecipe() 31 | @JvmField 32 | val generator = GeneratorRecipe() 33 | } 34 | 35 | init { 36 | Core.app.run { all.add(this@RecipeType) } 37 | } 38 | 39 | /**此类型的ID,必须是唯一的,此类型的所有实例共用此id*/ 40 | abstract val id: Int 41 | 42 | /**生成[配方视图][RecipeView]前对上下文数据进行初始化,并计算布局尺寸 43 | * 44 | * @return 表示该布局的长宽尺寸的二元向量 45 | */ 46 | abstract fun initial(recipe: Recipe): Vec2 47 | 48 | /**为参数传入的[RecipeNode]设置坐标以完成布局 */ 49 | abstract fun layout(recipeNode: RecipeNode) 50 | 51 | /**生成从给定起始节点到目标节点的[线条信息][tmi.ui.RecipeView.LineMeta] */ 52 | abstract fun line(from: RecipeNode, to: RecipeNode): LineMeta 53 | 54 | /**向配方显示器内添加显示部件的入口 */ 55 | open fun buildView(view: Group) {} 56 | 57 | open fun buildBack(background: Group){} 58 | 59 | fun drawLine(recipeView: RecipeView) { 60 | Draw.scl(recipeView.scaleX, recipeView.scaleY) 61 | 62 | for (line in recipeView.lines) { 63 | if (line.vertices.size < 2) continue 64 | 65 | val a = Draw.getColor().a 66 | Lines.stroke(Scl.scl(5f)*recipeView.scaleX, line.color.get()) 67 | Draw.alpha(Draw.getColor().a*a) 68 | 69 | if (line.vertices.size <= 4) { 70 | val x1 = line.vertices.items[0] - recipeView.width/2 71 | val y1 = line.vertices.items[1] - recipeView.height/2 72 | val x2 = line.vertices.items[2] - recipeView.width/2 73 | val y2 = line.vertices.items[3] - recipeView.height/2 74 | Lines.line( 75 | recipeView.x + recipeView.width/2 + x1*recipeView.scaleX, 76 | recipeView.y + recipeView.height/2 + y1*recipeView.scaleY, 77 | recipeView.x + recipeView.width/2 + x2*recipeView.scaleX, 78 | recipeView.y + recipeView.height/2 + y2*recipeView.scaleY 79 | ) 80 | continue 81 | } 82 | 83 | Lines.beginLine() 84 | var i = 0 85 | while (i < line.vertices.size) { 86 | val x1 = line.vertices.items[i] - recipeView.width/2 87 | val y1 = line.vertices.items[i + 1] - recipeView.height/2 88 | 89 | Lines.linePoint( 90 | recipeView.x + recipeView.width/2 + x1*recipeView.scaleX, 91 | recipeView.y + recipeView.height/2 + y1*recipeView.scaleY 92 | ) 93 | i += 2 94 | } 95 | Lines.endLine() 96 | } 97 | 98 | Draw.reset() 99 | } 100 | 101 | final override fun hashCode() = id 102 | 103 | override fun equals(other: Any?): Boolean { 104 | if (this === other) return true 105 | if (javaClass != other?.javaClass) return false 106 | return true 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/RecipesManager.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe 2 | 3 | import arc.func.Boolf 4 | import arc.struct.IntMap 5 | import arc.struct.ObjectSet 6 | import arc.struct.Seq 7 | import mindustry.Vars 8 | import mindustry.content.Items 9 | import mindustry.world.Block 10 | import tmi.TooManyItems 11 | import tmi.recipe.types.RecipeItem 12 | import tmi.util.Consts 13 | 14 | private val errorRecipe = Recipe( 15 | recipeType = RecipeType.factory, 16 | ownerBlock = TooManyItems.itemsManager.getByName("error") 17 | ).also { it.complete() } 18 | 19 | /**全局配方管理器,以单例模式运行,用于管理和查询所有已加载的配方和分组,同时[RecipeParser]也通过添加到该对象以生效 */ 20 | open class RecipesManager { 21 | /**所有[配方分析工具][RecipeParser]的存储容器,已注册的分析工具将被存储在这里待加载配方时分析方块使用 */ 22 | protected var parsers = Seq>(RecipeParser::class.java) 23 | 24 | /**当前存放所有配方的容器,所有已添加的配方都被存储在这里 */ 25 | protected val recipes = Seq() 26 | 27 | private val materials = ObjectSet>() 28 | private val productions = ObjectSet>() 29 | private val blocks = ObjectSet>() 30 | private val idMap = IntMap() 31 | 32 | /**向管理器注册一个[RecipeParser]用于分析方块可用的配方 */ 33 | fun registerParser(parser: RecipeParser<*>) { 34 | parsers.add(parser) 35 | } 36 | 37 | /**添加若干个配方 */ 38 | fun addRecipe(completeRecipe: Boolean = true, vararg recipes: Recipe) { 39 | for (recipe in recipes) { 40 | addRecipe(recipe, completeRecipe) 41 | } 42 | } 43 | 44 | /**添加若干个配方 */ 45 | fun addRecipe(recipes: Seq, completeRecipe: Boolean = true) { 46 | for (recipe in recipes) { 47 | addRecipe(recipe, completeRecipe) 48 | } 49 | } 50 | 51 | /**添加单个配方 */ 52 | fun addRecipe(recipe: Recipe, completeRecipe: Boolean = true) { 53 | if (completeRecipe) recipe.complete() 54 | 55 | idMap.put(recipe.hashCode(), recipe) 56 | 57 | recipes.add(recipe) 58 | for (stack in recipe.materials.values()) { 59 | materials.add(stack.item) 60 | } 61 | for (stack in recipe.productions.values()) { 62 | productions.add(stack.item) 63 | } 64 | if (recipe.ownerBlock != null) blocks.add(recipe.ownerBlock) 65 | } 66 | 67 | /**以配方的产出项筛选配方,若配方的产出物中包含参数给定的项目则添加到返回列表 */ 68 | fun getRecipesByProduction(production: RecipeItem<*>): Seq { 69 | return recipes.select { e: Recipe -> e.containsProduction(production) || (e.recipeType == RecipeType.building && e.ownerBlock == production) } 70 | } 71 | 72 | /**以配方的材料筛选配方,若配方的消耗材料中包含参数给定的项目则添加到返回列表 */ 73 | fun getRecipesByMaterial(material: RecipeItem<*>): Seq { 74 | return recipes.select { e: Recipe -> e.containsMaterial(material) } 75 | } 76 | 77 | /**以配方的建筑方块筛选配方,若配方的[Recipe.ownerBlock]与给定的参数相同则添加到返回列表 */ 78 | fun getRecipesByFactory(block: RecipeItem<*>): Seq { 79 | return recipes.select { e: Recipe -> e.recipeType != RecipeType.building && e.ownerBlock == block } 80 | } 81 | 82 | /**提供一个方块条目,搜索目标方块的建筑配方*/ 83 | fun getRecipesByBuilding(block: RecipeItem<*>): Seq { 84 | return recipes.select { e: Recipe -> e.recipeType == RecipeType.building && e.ownerBlock == block } 85 | } 86 | 87 | fun getByID(id: Int): Recipe { 88 | return idMap[id]?: errorRecipe 89 | } 90 | 91 | fun filterRecipe(filter: Boolf): Seq{ 92 | return recipes.select(filter) 93 | } 94 | 95 | fun init() { 96 | for (parser in parsers) { 97 | parser.init() 98 | } 99 | 100 | parseRecipes() 101 | 102 | val list = getRecipesByProduction(TooManyItems.itemsManager.getItem(Items.sand)) 103 | .select{ it.recipeType == RecipeType.collecting } 104 | 105 | list.forEach { 106 | println(it.hashCode()) 107 | } 108 | } 109 | 110 | /**从当前游戏内已装载的所有方块进行分析,搜索合适的[RecipeParser]解释方块以获取配方信息并添加到列表之中。 111 | * 另外,将方块的建造成本生成为配方添加到列表。 */ 112 | @Suppress("UNCHECKED_CAST") 113 | fun parseRecipes() { 114 | Vars.content.blocks().forEach { block -> 115 | parsers.forEach t@{ parser -> 116 | if (!parser.isTarget(block)) return@t 117 | 118 | parsers.forEach a@{ par -> 119 | if (par == parser) return@a 120 | if (par.isTarget(block) && parsers.contains { e -> e.isTarget(block) && e.exclude(parser) }) 121 | return@t 122 | } 123 | 124 | addRecipe((parser as RecipeParser).parse(block)) 125 | } 126 | 127 | if (block.requirements.isNotEmpty() && block.placeablePlayer) { 128 | val recipe = Recipe( 129 | RecipeType.building, 130 | TooManyItems.itemsManager.getItem(block), 131 | Consts.buildTimeAlter.get(block) as Float 132 | ) 133 | 134 | for (stack in block.requirements) { 135 | recipe.addMaterialInteger(TooManyItems.itemsManager.getItem(stack.item), stack.amount) 136 | } 137 | addRecipe(recipe) 138 | } 139 | } 140 | } 141 | 142 | /**参数给定的条目是否以输入材料的位置参与至少一个配方 */ 143 | fun anyMaterial(uc: RecipeItem<*>?): Boolean { 144 | return materials.contains(uc) 145 | } 146 | 147 | /**参数给定的条目是否以产出物的位置参与至少一个配方 */ 148 | fun anyProduction(uc: RecipeItem<*>?): Boolean { 149 | return productions.contains(uc) 150 | } 151 | 152 | /**是否有任意一个配方的方块项与参数给定的方块一致 */ 153 | fun anyBlock(uc: RecipeItem<*>?): Boolean { 154 | return blocks.contains(uc) 155 | } 156 | 157 | /**参数提供的项目是否可以找到任何一个其参与的配方 */ 158 | fun anyRecipe(uc: RecipeItem<*>): Boolean { 159 | return materials.contains(uc) || productions.contains(uc) || (uc.item is Block && blocks.contains(uc)) 160 | } 161 | } 162 | 163 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/AttributeCrafterParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.math.Mathf 4 | import arc.struct.Seq 5 | import mindustry.Vars 6 | import mindustry.world.Block 7 | import mindustry.world.blocks.environment.Floor 8 | import mindustry.world.blocks.production.AttributeCrafter 9 | import tmi.recipe.Recipe 10 | import tmi.recipe.Recipe.Companion.getDefaultEff 11 | import tmi.recipe.RecipeParser.Companion.getWrap 12 | import tmi.recipe.RecipeType 13 | import kotlin.math.min 14 | 15 | open class AttributeCrafterParser : ConsumerParser() { 16 | init { 17 | excludes.add(GenericCrafterParser::class.java) 18 | } 19 | 20 | override fun isTarget(content: Block): Boolean { 21 | return content is AttributeCrafter 22 | } 23 | 24 | override fun parse(content: AttributeCrafter): Seq { 25 | val res = Recipe( 26 | recipeType = RecipeType.factory, 27 | ownerBlock = content.getWrap(), 28 | craftTime = content.craftTime, 29 | ).setEff(getDefaultEff(content.baseEfficiency)) 30 | 31 | registerCons(res, *content.consumers) 32 | 33 | for (block in Vars.content.blocks()) { 34 | if (content.attribute == null || block.attributes[content.attribute] <= 0 || (block is Floor && block.isDeep)) continue 35 | 36 | val eff = min( 37 | (content.boostScale*content.size*content.size*block.attributes[content.attribute]), 38 | content.maxBoost 39 | ) 40 | 41 | res.addMaterial(block.getWrap(), (content.size*content.size) as Number) 42 | .setAttribute() 43 | .setOptional(content.baseEfficiency > 0.001f) 44 | .setEff(eff) 45 | .setFormat { "[#98ffa9]" + (if (content.baseEfficiency > 0.001f) "+" else "") + Mathf.round(eff*100) + "%" } 46 | } 47 | 48 | if (content.outputItems == null) { 49 | if (content.outputItem != null) res.addProductionInteger(content.outputItem.item.getWrap(), content.outputItem.amount) 50 | } 51 | else { 52 | for (item in content.outputItems) { 53 | res.addProductionInteger(item.item.getWrap(), item.amount) 54 | } 55 | } 56 | 57 | if (content.outputLiquids == null) { 58 | if (content.outputLiquid != null) res.addProductionPersec( 59 | content.outputLiquid.liquid.getWrap(), 60 | content.outputLiquid.amount 61 | ) 62 | } 63 | else { 64 | for (liquid in content.outputLiquids) { 65 | res.addProductionPersec(liquid.liquid.getWrap(), liquid.amount) 66 | } 67 | } 68 | 69 | return Seq.with(res) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/BeamDrillParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.math.Mathf 4 | import arc.struct.ObjectMap 5 | import arc.struct.ObjectSet 6 | import arc.struct.Seq 7 | import arc.util.Strings 8 | import mindustry.Vars 9 | import mindustry.core.UI 10 | import mindustry.type.Item 11 | import mindustry.world.Block 12 | import mindustry.world.blocks.environment.Floor 13 | import mindustry.world.blocks.environment.OreBlock 14 | import mindustry.world.blocks.production.BeamDrill 15 | import mindustry.world.consumers.Consume 16 | import mindustry.world.consumers.ConsumeLiquidBase 17 | import tmi.recipe.Recipe 18 | import tmi.recipe.RecipeItemStack 19 | import tmi.recipe.RecipeType 20 | import tmi.util.Consts.markerTile 21 | import tmi.util.Utils 22 | 23 | open class BeamDrillParser : ConsumerParser() { 24 | private var itemDrops: ObjectSet = ObjectSet() 25 | 26 | override fun init() { 27 | for (block in Vars.content.blocks()) { 28 | if (block is Floor && block.wallOre && block.itemDrop != null) itemDrops.add(block) 29 | } 30 | } 31 | 32 | override fun isTarget(content: Block): Boolean { 33 | return content is BeamDrill 34 | } 35 | 36 | override fun parse(content: BeamDrill): Seq { 37 | val res = ObjectMap() 38 | 39 | for (drop in itemDrops) { 40 | if (drop is OreBlock) markerTile.setOverlay(drop) 41 | else markerTile.setFloor(drop) 42 | 43 | if (drop.itemDrop.hardness > content.tier) continue 44 | 45 | val recipe = res.get(drop.itemDrop) { 46 | val r = Recipe( 47 | recipeType = RecipeType.collecting, 48 | ownerBlock = content.getWrap(), 49 | craftTime = content.getDrillTime(drop.itemDrop)/content.size, 50 | ).setEff(Recipe.zeroEff) 51 | 52 | r.addProductionInteger(drop.itemDrop.getWrap(), 1) 53 | 54 | if (content.optionalBoostIntensity != 1f) { 55 | registerCons( 56 | r, *Seq.with(*content.consumers).select { e: Consume -> !e.booster }.toArray( 57 | Consume::class.java 58 | ) 59 | ) 60 | val consBase = content.findConsumer { e: Consume -> e.booster } 61 | if (consBase is ConsumeLiquidBase) { 62 | registerCons(r, { s: RecipeItemStack<*> -> 63 | s.setEff(content.optionalBoostIntensity) 64 | .setBooster() 65 | .setOptional() 66 | .setFormat { f -> 67 | val (value, unit) = Utils.unitTimed(f) 68 | 69 | """ 70 | ${if (value > 1000) UI.formatAmount(value.toLong()) else Strings.autoFixed(value, 2)}$unit 71 | [#98ffa9]+${Mathf.round(content.optionalBoostIntensity*100)}% 72 | """.trimIndent() 73 | } 74 | }, consBase) 75 | } 76 | } 77 | else { 78 | registerCons(r, *content.consumers) 79 | } 80 | r 81 | } 82 | 83 | val realDrillTime = content.getDrillTime(drop.itemDrop) 84 | recipe!!.addMaterial(drop.getWrap(), content.size as Number) 85 | .setEff(content.drillTime/realDrillTime) 86 | .setAttribute() 87 | .emptyFormat() 88 | } 89 | 90 | return res.values().toSeq() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/ConsGeneratorParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.struct.Seq 4 | import mindustry.world.Block 5 | import mindustry.world.blocks.power.ConsumeGenerator 6 | import tmi.recipe.Recipe 7 | import tmi.recipe.RecipeType 8 | import tmi.recipe.types.PowerMark 9 | 10 | open class ConsGeneratorParser : ConsumerParser() { 11 | init { 12 | excludes.add(GeneratorParser::class.java) 13 | } 14 | 15 | override fun isTarget(content: Block): Boolean { 16 | return content is ConsumeGenerator 17 | } 18 | 19 | override fun parse(content: ConsumeGenerator): Seq { 20 | val res = Recipe( 21 | recipeType = RecipeType.generator, 22 | ownerBlock = content.getWrap() 23 | ) 24 | 25 | registerCons(res, *content.consumers) 26 | 27 | res.addProductionPersec(PowerMark, content.powerProduction) 28 | 29 | if (content.outputLiquid != null) { 30 | res.addProductionPersec(content.outputLiquid.liquid.getWrap(), content.outputLiquid.amount) 31 | } 32 | 33 | return Seq.with(res) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/ConstructorParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.struct.Seq 4 | import mindustry.Vars 5 | import mindustry.world.Block 6 | import mindustry.world.blocks.payloads.Constructor 7 | import tmi.recipe.Recipe 8 | import tmi.recipe.RecipeParser 9 | import tmi.recipe.RecipeType 10 | import tmi.util.Consts 11 | 12 | open class ConstructorParser: RecipeParser() { 13 | override fun isTarget(content: Block): Boolean { 14 | return content is Constructor 15 | } 16 | 17 | override fun parse(content: Constructor): Seq { 18 | val res = Seq() 19 | Vars.content.blocks() 20 | .select { content.canProduce(it) } 21 | .forEach { b -> 22 | val recipe = Recipe( 23 | recipeType = RecipeType.factory, 24 | ownerBlock = content.getWrap(), 25 | craftTime = (Consts.buildTimeAlter.get(b) as Float)/content.buildSpeed 26 | ) 27 | 28 | recipe.addProductionInteger(b.getWrap(), 1) 29 | 30 | b.requirements.forEach { stack -> 31 | recipe.addMaterialInteger(stack.item.getWrap(), stack.amount) 32 | } 33 | 34 | res.add(recipe) 35 | } 36 | 37 | return res 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/ConsumerParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.func.Boolf 4 | import arc.func.Cons 5 | import arc.func.Cons3 6 | import arc.struct.ObjectMap 7 | import mindustry.Vars 8 | import mindustry.world.Block 9 | import mindustry.world.consumers.* 10 | import tmi.invoke 11 | import tmi.recipe.Recipe 12 | import tmi.recipe.RecipeItemStack 13 | import tmi.recipe.RecipeParser 14 | import tmi.recipe.types.PowerMark 15 | 16 | abstract class ConsumerParser : RecipeParser() { 17 | protected fun registerCons(recipe: Recipe, vararg cons: Consume) { 18 | registerCons(recipe, {}, *cons) 19 | } 20 | 21 | protected fun registerCons(recipe: Recipe, handle: Cons>, vararg cons: Consume) { 22 | for (consume in cons) { 23 | for (entry in vanillaConsParser) { 24 | if (entry.key[consume]) entry.value[recipe, consume, handle] 25 | } 26 | } 27 | } 28 | 29 | companion object { 30 | protected var vanillaConsParser: ObjectMap, Cons3>>> = 31 | ObjectMap() 32 | 33 | fun registerVanillaConsParser(type: Boolf, handle: Cons3>>) { 34 | vanillaConsParser.put(type, handle) 35 | } 36 | 37 | fun registerVanillaConsumeParser() { 38 | //items 39 | registerVanillaConsParser( 40 | { c -> c is ConsumeItems }, 41 | { recipe: Recipe, consume: Consume, handle -> 42 | for (item in (consume as ConsumeItems).items) { 43 | handle( 44 | recipe.addMaterialInteger(item.item.getWrap(), item.amount) 45 | .setOptional(consume.optional) 46 | ) 47 | } 48 | }) 49 | registerVanillaConsParser( 50 | { c -> c is ConsumeItemFilter }, 51 | { recipe, consume, handle -> 52 | val cf = (consume as ConsumeItemFilter) 53 | for (item in Vars.content.items().select { i -> cf.filter[i] }) { 54 | handle( 55 | recipe.addMaterialInteger(item.getWrap(), 1) 56 | .setOptional(consume.optional) 57 | .setAttribute(cf) 58 | .setMaxAttr() 59 | ) 60 | } 61 | }) 62 | 63 | //liquids 64 | registerVanillaConsParser( 65 | { c -> c is ConsumeLiquids }, 66 | { recipe, consume, handle -> 67 | for (liquid in (consume as ConsumeLiquids).liquids) { 68 | handle(recipe.addMaterialPersec(liquid.liquid.getWrap(), liquid.amount) 69 | .setOptional(consume.optional)) 70 | } 71 | }) 72 | registerVanillaConsParser( 73 | { c -> c is ConsumeLiquid }, 74 | { recipe, consume, handle -> 75 | handle(recipe.addMaterialPersec((consume as ConsumeLiquid).liquid.getWrap(), consume.amount) 76 | .setOptional(consume.optional)) 77 | }) 78 | registerVanillaConsParser( 79 | { c -> c is ConsumeLiquidFilter }, 80 | { recipe, consume, handle -> 81 | val cf = (consume as ConsumeLiquidFilter) 82 | for (liquid in Vars.content.liquids().select { i -> cf.filter[i] }) { 83 | handle(recipe.addMaterialPersec(liquid.getWrap(), cf.amount) 84 | .setOptional(consume.optional) 85 | .setAttribute(cf) 86 | .setMaxAttr()) 87 | } 88 | }) 89 | 90 | //payloads 91 | registerVanillaConsParser( 92 | { c -> c is ConsumePayloads }, 93 | { recipe, consume, handle -> 94 | for (stack in (consume as ConsumePayloads).payloads) { 95 | if (stack.amount > 1) handle(recipe.addMaterialInteger(stack.item.getWrap(), stack.amount)) 96 | else handle( 97 | recipe.addMaterialInteger(stack.item.getWrap(), 1) 98 | .setOptional(consume.optional) 99 | ) 100 | } 101 | }) 102 | 103 | //power 104 | registerVanillaConsParser( 105 | { c -> c is ConsumePower }, 106 | { recipe, consume, handle -> 107 | handle( 108 | recipe.addMaterialPersec(PowerMark, (consume as ConsumePower).usage) 109 | .setOptional(consume.optional) 110 | ) 111 | }) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/DrillParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.math.Mathf 4 | import arc.struct.ObjectMap 5 | import arc.struct.ObjectSet 6 | import arc.struct.Seq 7 | import arc.util.Strings 8 | import mindustry.Vars 9 | import mindustry.core.UI 10 | import mindustry.type.Item 11 | import mindustry.world.Block 12 | import mindustry.world.blocks.environment.Floor 13 | import mindustry.world.blocks.environment.OreBlock 14 | import mindustry.world.blocks.production.Drill 15 | import mindustry.world.consumers.Consume 16 | import mindustry.world.consumers.ConsumeLiquidBase 17 | import tmi.recipe.Recipe 18 | import tmi.recipe.RecipeType 19 | import tmi.util.Consts.markerTile 20 | import tmi.util.Utils 21 | 22 | open class DrillParser : ConsumerParser() { 23 | protected var itemDrops: ObjectSet = ObjectSet() 24 | 25 | override fun init() { 26 | for (block in Vars.content.blocks()) { 27 | if (block is Floor && block.itemDrop != null && !block.wallOre) itemDrops.add(block) 28 | } 29 | } 30 | 31 | override fun isTarget(content: Block): Boolean { 32 | return content is Drill 33 | } 34 | 35 | override fun parse(content: Drill): Seq { 36 | val res = ObjectMap() 37 | 38 | for (drop in itemDrops) { 39 | if (drop is OreBlock) markerTile.setOverlay(drop) 40 | else markerTile.setFloor(drop) 41 | 42 | if (!content.canMine(markerTile)) continue 43 | 44 | val recipe = res.get(drop.itemDrop) { 45 | val r = Recipe( 46 | recipeType = RecipeType.collecting, 47 | ownerBlock = content.getWrap(), 48 | craftTime = content.getDrillTime(drop.itemDrop)/content.size/content.size, 49 | ).setEff(Recipe.zeroEff) 50 | 51 | r.addProductionInteger(drop.itemDrop.getWrap(), 1) 52 | 53 | if (content.liquidBoostIntensity != 1f) { 54 | registerCons(r, *Seq.with(*content.consumers).select { e: Consume -> !(e.optional && e is ConsumeLiquidBase && e.booster) }.toArray(Consume::class.java)) 55 | 56 | val consBase = content.findConsumer { f: Consume -> f is ConsumeLiquidBase && f.optional && f.booster } 57 | if (consBase is ConsumeLiquidBase) { 58 | registerCons(r, { s -> 59 | val eff = content.liquidBoostIntensity*content.liquidBoostIntensity 60 | s!!.setEff(eff) 61 | .setBooster() 62 | .setOptional() 63 | .setFormat { f -> 64 | val (value, unit) = Utils.unitTimed(f) 65 | 66 | """ 67 | ${if (value > 1000) UI.formatAmount(value.toLong()) else Strings.autoFixed(value, 2)}$unit 68 | [#98ffa9]+${Mathf.round(eff*100)}% 69 | """.trimIndent() 70 | } 71 | }, consBase) 72 | } 73 | } 74 | else { 75 | registerCons(r, *content.consumers) 76 | } 77 | r 78 | } 79 | 80 | val realDrillTime = content.getDrillTime(drop.itemDrop) 81 | recipe!!.addMaterial(drop.getWrap(), (content.size*content.size) as Number) 82 | .setEff(content.drillTime/realDrillTime) 83 | .setAttribute() 84 | .emptyFormat() 85 | } 86 | 87 | return res.values().toSeq() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/FrackerParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.math.Mathf 4 | import arc.struct.Seq 5 | import mindustry.Vars 6 | import mindustry.world.Block 7 | import mindustry.world.blocks.environment.Floor 8 | import mindustry.world.blocks.production.Fracker 9 | import tmi.recipe.Recipe 10 | import tmi.recipe.Recipe.Companion.getDefaultEff 11 | import tmi.recipe.RecipeType 12 | 13 | open class FrackerParser : ConsumerParser() { 14 | init { 15 | excludes.add(PumpParser::class.java) 16 | excludes.add(SolidPumpParser::class.java) 17 | } 18 | 19 | override fun isTarget(content: Block): Boolean { 20 | return content is Fracker 21 | } 22 | 23 | override fun parse(content: Fracker): Seq { 24 | val res = Recipe( 25 | recipeType = RecipeType.collecting, 26 | ownerBlock = content.getWrap(), 27 | craftTime = content.consumeTime, 28 | ).setEff(getDefaultEff(content.baseEfficiency)) 29 | 30 | res.addProductionPersec(content.result.getWrap(), content.pumpAmount) 31 | 32 | registerCons(res, *content.consumers) 33 | 34 | for (block in Vars.content.blocks()) { 35 | if (content.attribute == null || block.attributes[content.attribute] <= 0 || (block is Floor && block.isDeep)) continue 36 | 37 | val eff = block.attributes[content.attribute] 38 | res.addMaterial(block.getWrap(), (content.size*content.size) as Number) 39 | .setOptional(content.baseEfficiency > 0.001f) 40 | .setEff(eff) 41 | .setAttribute() 42 | .setFormat { "[#98ffa9]" + (if (content.baseEfficiency > 0.001f) "+" else "") + Mathf.round(eff*100) + "%" } 43 | } 44 | 45 | return Seq.with(res) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/GeneratorParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.struct.Seq 4 | import mindustry.world.Block 5 | import mindustry.world.blocks.power.PowerGenerator 6 | import tmi.recipe.Recipe 7 | import tmi.recipe.RecipeParser.Companion.getWrap 8 | import tmi.recipe.RecipeType 9 | import tmi.recipe.types.PowerMark 10 | 11 | open class GeneratorParser : ConsumerParser() { 12 | override fun isTarget(content: Block): Boolean { 13 | return content is PowerGenerator 14 | } 15 | 16 | override fun parse(content: PowerGenerator): Seq { 17 | val res = Recipe( 18 | recipeType = RecipeType.generator, 19 | ownerBlock = content.getWrap() 20 | ) 21 | 22 | registerCons(res, *content.consumers) 23 | 24 | res.addProductionPersec(PowerMark, content.powerProduction) 25 | 26 | return Seq.with(res) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/GenericCrafterParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.struct.Seq 4 | import mindustry.world.Block 5 | import mindustry.world.blocks.production.GenericCrafter 6 | import tmi.recipe.Recipe 7 | import tmi.recipe.RecipeType 8 | 9 | open class GenericCrafterParser : ConsumerParser() { 10 | override fun isTarget(content: Block): Boolean { 11 | return content is GenericCrafter 12 | } 13 | 14 | override fun parse(content: GenericCrafter): Seq { 15 | val res = Recipe( 16 | recipeType = RecipeType.factory, 17 | ownerBlock = content.getWrap(), 18 | craftTime = content.craftTime 19 | ) 20 | 21 | registerCons(res, *content.consumers) 22 | 23 | if (content.outputItems == null) { 24 | if (content.outputItem != null) res.addProductionInteger(content.outputItem.item.getWrap(), content.outputItem.amount) 25 | } 26 | else { 27 | for (item in content.outputItems) { 28 | res.addProductionInteger(item.item.getWrap(), item.amount) 29 | } 30 | } 31 | 32 | if (content.outputLiquids == null) { 33 | if (content.outputLiquid != null) res.addProductionPersec( 34 | content.outputLiquid.liquid.getWrap(), 35 | content.outputLiquid.amount 36 | ) 37 | } 38 | else { 39 | for (liquid in content.outputLiquids) { 40 | res.addProductionPersec(liquid.liquid.getWrap(), liquid.amount) 41 | } 42 | } 43 | 44 | return Seq.with(res) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/HeatCrafterParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.struct.Seq 4 | import mindustry.world.Block 5 | import mindustry.world.blocks.production.HeatCrafter 6 | import tmi.recipe.Recipe 7 | import tmi.recipe.RecipeType 8 | import tmi.recipe.types.HeatMark 9 | 10 | open class HeatCrafterParser : ConsumerParser() { 11 | init { 12 | excludes.add(GenericCrafterParser::class.java) 13 | } 14 | 15 | override fun isTarget(content: Block): Boolean { 16 | return content is HeatCrafter 17 | } 18 | 19 | override fun parse(content: HeatCrafter): Seq { 20 | val res = Recipe( 21 | recipeType = RecipeType.factory, 22 | ownerBlock = content.getWrap(), 23 | craftTime = content.craftTime, 24 | ) 25 | 26 | res.addMaterial(HeatMark, content.heatRequirement as Number).floatFormat() 27 | 28 | registerCons(res, *content.consumers) 29 | 30 | if (content.outputItems == null) { 31 | if (content.outputItem != null) res.addProductionInteger( 32 | content.outputItem.item.getWrap(), content.outputItem.amount 33 | ) 34 | } 35 | else { 36 | for (item in content.outputItems) { 37 | res.addProductionInteger(item.item.getWrap(), item.amount) 38 | } 39 | } 40 | 41 | if (content.outputLiquids == null) { 42 | if (content.outputLiquid != null) res.addProductionPersec( 43 | content.outputLiquid.liquid.getWrap(), content.outputLiquid.amount 44 | ) 45 | } 46 | else { 47 | for (liquid in content.outputLiquids) { 48 | res.addProductionPersec(liquid.liquid.getWrap(), liquid.amount) 49 | } 50 | } 51 | 52 | return Seq.with(res) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/HeatGeneratorParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.struct.Seq 4 | import mindustry.world.Block 5 | import mindustry.world.blocks.power.HeaterGenerator 6 | import tmi.recipe.Recipe 7 | import tmi.recipe.RecipeType 8 | import tmi.recipe.types.HeatMark 9 | import tmi.recipe.types.PowerMark 10 | 11 | open class HeatGeneratorParser : ConsumerParser() { 12 | init { 13 | excludes.add(GeneratorParser::class.java) 14 | excludes.add(ConsGeneratorParser::class.java) 15 | } 16 | 17 | override fun isTarget(content: Block): Boolean { 18 | return content is HeaterGenerator 19 | } 20 | 21 | override fun parse(content: HeaterGenerator): Seq { 22 | val res = Recipe( 23 | recipeType = RecipeType.generator, 24 | ownerBlock = content.getWrap(), 25 | craftTime = content.itemDuration, 26 | ) 27 | 28 | registerCons(res, *content.consumers) 29 | 30 | res.addProductionPersec(PowerMark, content.powerProduction) 31 | 32 | if (content.heatOutput > 0) { 33 | res.addProduction(HeatMark, content.heatOutput as Number).floatFormat() 34 | } 35 | 36 | if (content.outputLiquid != null) { 37 | res.addProductionPersec(content.outputLiquid.liquid.getWrap(), content.outputLiquid.amount) 38 | } 39 | 40 | return Seq.with(res) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/HeatProducerParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.struct.Seq 4 | import mindustry.world.Block 5 | import mindustry.world.blocks.heat.HeatProducer 6 | import tmi.recipe.Recipe 7 | import tmi.recipe.RecipeType 8 | import tmi.recipe.types.HeatMark 9 | 10 | open class HeatProducerParser : ConsumerParser() { 11 | init { 12 | excludes.add(GenericCrafterParser::class.java) 13 | } 14 | 15 | override fun isTarget(content: Block): Boolean { 16 | return content is HeatProducer 17 | } 18 | 19 | override fun parse(content: HeatProducer): Seq { 20 | val res = Recipe( 21 | recipeType = RecipeType.factory, 22 | ownerBlock = 0.getWrap(), 23 | craftTime = content.craftTime, 24 | ) 25 | 26 | registerCons(res, *content.consumers) 27 | 28 | res.addProduction(HeatMark, content.heatOutput as Number).floatFormat() 29 | 30 | if (content.outputItems == null) { 31 | if (content.outputItem != null) res.addProductionInteger(content.outputItem.item.getWrap(), content.outputItem.amount) 32 | } 33 | else { 34 | for (item in content.outputItems) { 35 | res.addProductionInteger(item.item.getWrap(), item.amount) 36 | } 37 | } 38 | 39 | if (content.outputLiquids == null) { 40 | if (content.outputLiquid != null) res.addProductionPersec( 41 | content.outputLiquid.liquid.getWrap(), 42 | content.outputLiquid.amount 43 | ) 44 | } 45 | else { 46 | for (liquid in content.outputLiquids) { 47 | res.addProductionPersec(liquid.liquid.getWrap(), liquid.amount) 48 | } 49 | } 50 | 51 | return Seq.with(res) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/PumpParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.struct.ObjectMap 4 | import arc.struct.ObjectSet 5 | import arc.struct.Seq 6 | import mindustry.Vars 7 | import mindustry.type.Liquid 8 | import mindustry.world.Block 9 | import mindustry.world.Tile 10 | import mindustry.world.blocks.environment.Floor 11 | import mindustry.world.blocks.production.Pump 12 | import tmi.recipe.Recipe 13 | import tmi.recipe.RecipeType 14 | import tmi.util.Consts.markerTile 15 | import java.lang.reflect.InvocationTargetException 16 | 17 | open class PumpParser : ConsumerParser() { 18 | private val floorDrops: ObjectSet = ObjectSet() 19 | 20 | override fun init() { 21 | for (block in Vars.content.blocks()) { 22 | if (block is Floor && block.liquidDrop != null) floorDrops.add(block) 23 | } 24 | } 25 | 26 | override fun isTarget(content: Block): Boolean { 27 | return content is Pump 28 | } 29 | 30 | override fun parse(content: Pump): Seq { 31 | val res = ObjectMap() 32 | for (drop in floorDrops) { 33 | markerTile.setFloor(drop) 34 | try { 35 | if (!(canPump.invoke(content, markerTile) as Boolean)) continue 36 | } catch (e: IllegalAccessException) { 37 | throw RuntimeException(e) 38 | } catch (e: InvocationTargetException) { 39 | throw RuntimeException(e) 40 | } 41 | 42 | val recipe = res[drop.liquidDrop, { 43 | val r = Recipe( 44 | recipeType = RecipeType.collecting, 45 | craftTime = content.consumeTime, 46 | ownerBlock = content.getWrap() 47 | ).setEff(Recipe.zeroEff) 48 | 49 | r.addProductionPersec(drop.liquidDrop.getWrap(), content.pumpAmount*content.size*content.size) //WTF? 50 | registerCons(r, *content.consumers) 51 | r 52 | }] 53 | 54 | recipe!!.addMaterial(drop.getWrap(), (content.size*content.size) as Number) 55 | .setAttribute() 56 | .emptyFormat() 57 | } 58 | return res.values().toSeq() 59 | } 60 | 61 | companion object { 62 | private val canPump by lazy { 63 | try { 64 | val res = Pump::class.java.getDeclaredMethod("canPump", Tile::class.java) 65 | res.setAccessible(true) 66 | return@lazy res 67 | } catch (e: NoSuchMethodException) { 68 | throw RuntimeException(e) 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/ReconstructorParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.struct.Seq 4 | import mindustry.world.Block 5 | import mindustry.world.blocks.units.Reconstructor 6 | import tmi.recipe.Recipe 7 | import tmi.recipe.RecipeType 8 | 9 | open class ReconstructorParser : ConsumerParser() { 10 | override fun isTarget(content: Block): Boolean { 11 | return content is Reconstructor 12 | } 13 | 14 | override fun parse(content: Reconstructor): Seq { 15 | val res = Seq() 16 | for (upgrade in content.upgrades) { 17 | val recipe = Recipe( 18 | recipeType = RecipeType.factory, 19 | ownerBlock = content.getWrap(), 20 | craftTime = content.constructTime, 21 | ) 22 | 23 | recipe.addMaterialInteger(upgrade[0].getWrap(), 1) 24 | recipe.addProductionInteger(upgrade[1].getWrap(), 1) 25 | 26 | registerCons(recipe, *content.consumers) 27 | 28 | res.add(recipe) 29 | } 30 | 31 | return res 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/SeparatorParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.math.Mathf 4 | import arc.struct.Seq 5 | import arc.util.Strings 6 | import mindustry.world.Block 7 | import mindustry.world.blocks.production.* 8 | import mindustry.world.meta.StatUnit 9 | import tmi.recipe.Recipe 10 | import tmi.recipe.RecipeType 11 | 12 | open class SeparatorParser : ConsumerParser() { 13 | override fun isTarget(content: Block): Boolean { 14 | return content is Separator 15 | } 16 | 17 | override fun parse(content: Separator): Seq { 18 | val res = Recipe( 19 | recipeType = RecipeType.factory, 20 | ownerBlock = content.getWrap(), 21 | craftTime = content.craftTime 22 | ) 23 | 24 | registerCons(res, *content.consumers) 25 | 26 | var n = 0f 27 | for (stack in content.results) { 28 | n += stack.amount.toFloat() 29 | } 30 | for (item in content.results) { 31 | res.addProduction(item.item.getWrap(), (item.amount/n/content.craftTime) as Number) 32 | .setFormat { f -> Mathf.round(f*100*res.craftTime).toString() + "%" } 33 | .setAltFormat { f -> Strings.autoFixed(f*60, 1) + StatUnit.perSecond.localized() } 34 | } 35 | 36 | return Seq.with(res) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/SolidPumpParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.math.Mathf 4 | import arc.struct.Seq 5 | import mindustry.Vars 6 | import mindustry.world.Block 7 | import mindustry.world.blocks.environment.Floor 8 | import mindustry.world.blocks.production.SolidPump 9 | import tmi.recipe.Recipe 10 | import tmi.recipe.Recipe.Companion.getDefaultEff 11 | import tmi.recipe.RecipeType 12 | 13 | open class SolidPumpParser : ConsumerParser() { 14 | init { 15 | excludes.add(PumpParser::class.java) 16 | } 17 | 18 | override fun isTarget(content: Block): Boolean { 19 | return content is SolidPump 20 | } 21 | 22 | override fun parse(content: SolidPump): Seq { 23 | val res = Recipe( 24 | recipeType = RecipeType.collecting, 25 | ownerBlock = content.getWrap(), 26 | craftTime = content.consumeTime 27 | ).setEff(getDefaultEff(content.baseEfficiency)) 28 | 29 | res.addProductionPersec(content.result.getWrap(), content.pumpAmount) 30 | 31 | registerCons(res, *content.consumers) 32 | 33 | for (block in Vars.content.blocks()) { 34 | if (content.attribute == null || block.attributes[content.attribute] <= 0 || (block is Floor && block.isDeep)) continue 35 | 36 | val eff = block.attributes[content.attribute] 37 | res.addMaterial(block.getWrap(), (content.size*content.size) as Number) 38 | .setOptional(content.baseEfficiency > 0.001f) 39 | .setEff(eff) 40 | .setAttribute() 41 | .setFormat { "[#98ffa9]" + (if (content.baseEfficiency > 0.001f) "+" else "") + Mathf.round(eff*100) + "%" } 42 | } 43 | 44 | return Seq.with(res) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/ThermalGeneratorParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.math.Mathf 4 | import arc.struct.Seq 5 | import mindustry.Vars 6 | import mindustry.world.Block 7 | import mindustry.world.blocks.power.ThermalGenerator 8 | import tmi.recipe.Recipe 9 | import tmi.recipe.RecipeType 10 | import tmi.recipe.types.PowerMark 11 | 12 | open class ThermalGeneratorParser : ConsumerParser() { 13 | init { 14 | excludes.add(GeneratorParser::class.java) 15 | } 16 | 17 | override fun isTarget(content: Block): Boolean { 18 | return content is ThermalGenerator 19 | } 20 | 21 | override fun parse(content: ThermalGenerator): Seq { 22 | val res = Recipe( 23 | recipeType = RecipeType.generator, 24 | ownerBlock = content.getWrap() 25 | ).setEff(Recipe.zeroEff) 26 | 27 | registerCons(res, *content.consumers) 28 | 29 | res.addProductionPersec(PowerMark, content.powerProduction) 30 | 31 | for (block in Vars.content.blocks()) { 32 | if (content.attribute == null || block.attributes[content.attribute] <= 0) continue 33 | 34 | val eff = content.displayEfficiencyScale*content.size*content.size*block.attributes[content.attribute] 35 | if (eff <= content.minEfficiency) continue 36 | res.addMaterial(block.getWrap(), (content.size*content.size) as Number) 37 | .setEff(eff) 38 | .setAttribute() 39 | .setFormat { "[#98ffa9]" + Mathf.round(eff*100) + "%" } 40 | } 41 | 42 | if (content.outputLiquid != null) res.addProductionPersec( 43 | content.outputLiquid.liquid.getWrap(), 44 | content.outputLiquid.amount 45 | ) 46 | 47 | return Seq.with(res) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/UnitAssemblerParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.struct.Seq 4 | import mindustry.world.Block 5 | import mindustry.world.blocks.units.UnitAssembler 6 | import tmi.recipe.Recipe 7 | import tmi.recipe.RecipeType 8 | 9 | open class UnitAssemblerParser : ConsumerParser() { 10 | override fun isTarget(content: Block): Boolean { 11 | return content is UnitAssembler 12 | } 13 | 14 | override fun parse(content: UnitAssembler): Seq { 15 | val res = Seq() 16 | 17 | for (plan in content.plans) { 18 | val recipe = Recipe( 19 | recipeType = RecipeType.factory, 20 | ownerBlock = content.getWrap(), 21 | craftTime = plan.time 22 | ) 23 | 24 | recipe.addProductionInteger(plan.unit.getWrap(), 1) 25 | 26 | for (stack in plan.requirements) { 27 | recipe.addMaterialInteger(stack.item.getWrap(), stack.amount) 28 | } 29 | 30 | registerCons(recipe, *content.consumers) 31 | 32 | res.add(recipe) 33 | } 34 | 35 | return res 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/UnitFactoryParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.struct.Seq 4 | import mindustry.world.Block 5 | import mindustry.world.blocks.units.UnitFactory 6 | import tmi.recipe.Recipe 7 | import tmi.recipe.RecipeType 8 | 9 | open class UnitFactoryParser : ConsumerParser() { 10 | override fun isTarget(content: Block): Boolean { 11 | return content is UnitFactory 12 | } 13 | 14 | override fun parse(content: UnitFactory): Seq { 15 | val res = Seq() 16 | 17 | for (plan in content.plans) { 18 | val recipe = Recipe( 19 | recipeType = RecipeType.factory, 20 | ownerBlock = content.getWrap(), 21 | craftTime = plan.time 22 | ) 23 | 24 | recipe.addProductionInteger(plan.unit.getWrap(), 1) 25 | 26 | for (stack in plan.requirements) { 27 | recipe.addMaterialInteger(stack.item.getWrap(), stack.amount) 28 | } 29 | 30 | registerCons(recipe, *content.consumers) 31 | 32 | res.add(recipe) 33 | } 34 | 35 | return res 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/VariableReactorParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.struct.Seq 4 | import mindustry.world.Block 5 | import mindustry.world.blocks.power.VariableReactor 6 | import tmi.recipe.Recipe 7 | import tmi.recipe.RecipeType 8 | import tmi.recipe.types.HeatMark 9 | import tmi.recipe.types.PowerMark 10 | 11 | open class VariableReactorParser : ConsumerParser() { 12 | init { 13 | excludes.add(GeneratorParser::class.java) 14 | } 15 | 16 | override fun isTarget(content: Block): Boolean { 17 | return content is VariableReactor 18 | } 19 | 20 | override fun parse(content: VariableReactor): Seq { 21 | val res = Recipe( 22 | recipeType = RecipeType.generator, 23 | ownerBlock = content.getWrap() 24 | ) 25 | 26 | registerCons(res, *content.consumers) 27 | 28 | res.addProductionPersec(PowerMark, content.powerProduction) 29 | 30 | if (content.maxHeat > 0) { 31 | res.addProduction(HeatMark, content.maxHeat as Number).floatFormat() 32 | } 33 | 34 | return Seq.with(res) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/parser/WallCrafterParser.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.parser 2 | 3 | import arc.math.Mathf 4 | import arc.struct.Seq 5 | import mindustry.Vars 6 | import mindustry.world.Block 7 | import mindustry.world.blocks.environment.Floor 8 | import mindustry.world.blocks.production.WallCrafter 9 | import tmi.recipe.Recipe 10 | import tmi.recipe.RecipeType 11 | 12 | open class WallCrafterParser : ConsumerParser() { 13 | override fun isTarget(content: Block): Boolean { 14 | return content is WallCrafter 15 | } 16 | 17 | override fun parse(content: WallCrafter): Seq { 18 | val res = Recipe( 19 | recipeType = RecipeType.collecting, 20 | ownerBlock = content.getWrap(), 21 | craftTime = content.drillTime 22 | ).setEff(Recipe.zeroEff) 23 | 24 | res.addProductionInteger(content.output.getWrap(), 1) 25 | 26 | registerCons(res, *content.consumers) 27 | 28 | for (block in Vars.content.blocks()) { 29 | if (content.attribute == null || block.attributes[content.attribute] <= 0 || (block is Floor && block.isDeep)) continue 30 | 31 | val eff = block.attributes[content.attribute] 32 | res.addMaterial(block.getWrap(), block.size as Number) 33 | .setEff(eff) 34 | .setAttribute() 35 | .setFormat { "[#98ffa9]" + Mathf.round(eff*100) + "%" } 36 | } 37 | 38 | return Seq.with(res) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/types/BuildingRecipe.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.types 2 | 3 | import arc.Core 4 | import arc.func.Prov 5 | import arc.graphics.Color 6 | import arc.math.Angles 7 | import arc.math.Mathf 8 | import arc.math.Rand 9 | import arc.math.geom.Vec2 10 | import arc.scene.Group 11 | import arc.scene.ui.ImageButton 12 | import arc.scene.ui.Label 13 | import arc.scene.ui.layout.Scl 14 | import arc.scene.ui.layout.Table 15 | import arc.struct.ObjectMap 16 | import arc.util.Align 17 | import arc.util.Log 18 | import arc.util.Scaling 19 | import arc.util.Strings 20 | import mindustry.Vars 21 | import mindustry.core.UI 22 | import mindustry.gen.Icon 23 | import mindustry.ui.Styles 24 | import mindustry.world.Block 25 | import mindustry.world.meta.StatUnit 26 | import tmi.recipe.Recipe 27 | import tmi.recipe.RecipeItemStack 28 | import tmi.recipe.RecipeType 29 | import tmi.set 30 | import tmi.ui.NODE_SIZE 31 | import tmi.ui.NodeType 32 | import tmi.ui.RecipeNode 33 | import tmi.ui.RecipeView.LineMeta 34 | import tmi.util.Consts 35 | import kotlin.math.max 36 | 37 | class BuildingRecipe : RecipeType() { 38 | val bound = Vec2() 39 | val blockPos = Vec2() 40 | val materialPos = ObjectMap, Vec2>() 41 | 42 | var time = 0f 43 | var build: Block? = null 44 | 45 | override val id = 0 46 | 47 | override fun buildView(view: Group) { 48 | val label = Label(Core.bundle["misc.building"], Styles.outlineLabel) 49 | label.style.background = Consts.grayUIAlpha 50 | label.validate() 51 | 52 | label.setPosition(blockPos.x + NODE_SIZE/2 + ITEM_PAD + label.prefWidth/2, blockPos.y, Align.center) 53 | view.addChild(label) 54 | 55 | if (time > 0) { 56 | val time = Table(Consts.grayUIAlpha) { t -> 57 | t.image(Consts.time).scaling(Scaling.fit).size(24f).pad(4f) 58 | t.add( 59 | (if (this.time > 3600) UI.formatTime(this.time) 60 | else Strings.autoFixed(this.time/60, 2) + StatUnit.seconds.localized()), 61 | Styles.outlineLabel 62 | ) 63 | } 64 | time.validate() 65 | 66 | time.setPosition( 67 | blockPos.x + NODE_SIZE/2 + ITEM_PAD + time.prefWidth/2, 68 | blockPos.y - label.height - 4, 69 | Align.center 70 | ) 71 | view.addChild(time) 72 | } 73 | 74 | if (Vars.state.isGame) { 75 | val button = ImageButton(Icon.hammer, Styles.clearNonei) 76 | 77 | button.setDisabled { 78 | build == null || !build!!.unlockedNow() || !build!!.placeablePlayer || !build!!.environmentBuildable() || !build!!.supportsEnv( 79 | Vars.state.rules.env 80 | ) 81 | } 82 | button.clicked { 83 | while (Core.scene.hasDialog()) { 84 | Core.scene.dialog.hide() 85 | } 86 | Vars.control.input.block = build 87 | } 88 | button.margin(5f) 89 | button.setSize(40f) 90 | button.setPosition(bound.x, 0f, Align.topRight) 91 | view.addChild(button) 92 | } 93 | } 94 | 95 | override fun initial(recipe: Recipe): Vec2 { 96 | build = recipe.ownerBlock!!.item as Block 97 | time = recipe.craftTime 98 | 99 | bound.setZero() 100 | blockPos.setZero() 101 | materialPos.clear() 102 | 103 | val seq: List> = recipe.materials.values().toList() 104 | val radians = 2f*Mathf.pi/seq.size 105 | val radius = max( 106 | MIN_RAD.toDouble(), 107 | ((NODE_SIZE + ITEM_PAD)/radians).toDouble() 108 | ).toFloat() 109 | 110 | bound.set(radius + NODE_SIZE, radius + NODE_SIZE).scl(2f) 111 | blockPos.set(bound).scl(0.5f) 112 | 113 | val r = Rand(build!!.id.toLong()) 114 | val off = r.random(0f, 360f) 115 | for (i in seq.indices) { 116 | val angle = radians*i*Mathf.radDeg + off 117 | val rot = r.random(0f, RAND) + radius 118 | 119 | materialPos[seq[i].item] = Vec2(blockPos.x + Angles.trnsx(angle, rot), blockPos.y + Angles.trnsy(angle, rot)) 120 | } 121 | 122 | return bound 123 | } 124 | 125 | override fun layout(recipeNode: RecipeNode) { 126 | when (recipeNode.type) { 127 | NodeType.MATERIAL -> { 128 | val pos = materialPos[recipeNode.stack.item] 129 | recipeNode.setPosition(pos.x, pos.y, Align.center) 130 | } 131 | NodeType.BLOCK -> { 132 | recipeNode.setPosition(blockPos.x, blockPos.y, Align.center) 133 | } 134 | else -> Log.warn("unexpected production in building recipe") 135 | } 136 | } 137 | 138 | override fun line(from: RecipeNode, to: RecipeNode): LineMeta { 139 | val res = LineMeta() 140 | res.color = Prov { Color.gray } 141 | 142 | val offX = from.width/2 143 | val offY = from.height/2 144 | val offX1 = to.width/2 145 | val offY1 = to.height/2 146 | 147 | res.addVertex(from.x + offX, from.y + offY) 148 | res.addVertex(to.x + offX1, to.y + offY1) 149 | 150 | return res 151 | } 152 | 153 | companion object { 154 | val ITEM_PAD = Scl.scl(30f) 155 | val RAND = Scl.scl(65f) 156 | val MIN_RAD = Scl.scl(125f) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/types/CollectingRecipe.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.types 2 | 3 | import arc.Core 4 | import arc.math.geom.Vec2 5 | import arc.scene.Group 6 | import arc.scene.ui.Label 7 | import arc.scene.ui.layout.Scl 8 | import arc.struct.ObjectMap 9 | import arc.util.Align 10 | import mindustry.ui.Styles 11 | import tmi.recipe.Recipe 12 | import tmi.recipe.RecipeItemStack 13 | import tmi.set 14 | import tmi.ui.NODE_SIZE 15 | import kotlin.math.max 16 | 17 | open class CollectingRecipe : FactoryRecipe() { 18 | override val id = 2 19 | 20 | override fun buildView(view: Group) { 21 | val label = Label(Core.bundle["misc.collecting"], Styles.outlineLabel) 22 | label.layout() 23 | 24 | label.setPosition(blockPos.x + NODE_SIZE/2 + ITEM_PAD + label.prefWidth/2, blockPos.y, Align.center) 25 | view.addChild(label) 26 | 27 | buildOpts(view) 28 | buildTime(view, label.height) 29 | } 30 | 31 | override fun initial(recipe: Recipe): Vec2 { 32 | time = recipe.craftTime 33 | 34 | consPos.clear() 35 | prodPos.clear() 36 | optPos.setZero() 37 | blockPos.setZero() 38 | 39 | val mats: List> = 40 | recipe.materials.values().filter { e -> !e.optionalCons } 41 | val opts: List> = 42 | recipe.materials.values().filter { e -> e.optionalCons } 43 | hasOptionals = opts.isNotEmpty() 44 | val materialNum = mats.size 45 | val productionNum = recipe.productions.size 46 | 47 | bound.setZero() 48 | 49 | var wOpt = 0f 50 | var wMat = 0f 51 | var wProd = 0f 52 | 53 | if (hasOptionals) { 54 | wOpt = handleBound(opts.size) 55 | bound.y += ROW_PAD 56 | } 57 | if (materialNum > 0) { 58 | wMat = handleBound(materialNum) 59 | bound.y += ROW_PAD 60 | } 61 | bound.y += NODE_SIZE 62 | if (productionNum > 0) { 63 | bound.y += ROW_PAD 64 | wProd = handleBound(productionNum) 65 | } 66 | 67 | val offOptX = (bound.x - wOpt)/2 68 | val offMatX = (bound.x - wMat)/2 69 | val offProdX = (bound.x - wProd)/2 70 | 71 | val centX = bound.x/2f 72 | var offY = NODE_SIZE/2 73 | 74 | if (hasOptionals) { 75 | offY = handleNode(opts, consPos, offOptX, offY) 76 | optPos[bound.x/2] = offY 77 | offY += ROW_PAD 78 | } 79 | if (materialNum > 0) { 80 | offY = handleNode(mats, consPos, offMatX, offY) 81 | offY += ROW_PAD 82 | } 83 | blockPos[centX] = offY 84 | offY += NODE_SIZE 85 | if (productionNum > 0) { 86 | offY += ROW_PAD 87 | val seq: List> = recipe.productions.values().toList() 88 | handleNode(seq, prodPos, offProdX, offY) 89 | } 90 | 91 | return bound 92 | } 93 | 94 | protected fun handleNode( 95 | seq: List>, 96 | pos: ObjectMap, Vec2>, 97 | offX: Float, 98 | offY: Float 99 | ): Float { 100 | var yOff = offY 101 | var dx = NODE_SIZE/2 102 | for (element in seq) { 103 | pos[element.item] = Vec2(offX + dx, yOff) 104 | dx += NODE_SIZE + ITEM_PAD 105 | } 106 | yOff += NODE_SIZE 107 | return yOff 108 | } 109 | 110 | protected fun handleBound(num: Int): Float { 111 | var res: Float 112 | 113 | bound.x = max( 114 | bound.x.toDouble(), 115 | (NODE_SIZE*num + ITEM_PAD*(num - 1)).also { 116 | res = it 117 | }.toDouble() 118 | ).toFloat() 119 | bound.y += NODE_SIZE 120 | 121 | return res 122 | } 123 | 124 | companion object { 125 | val ROW_PAD: Float = Scl.scl(60f) 126 | val ITEM_PAD: Float = Scl.scl(10f) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/types/FactoryRecipe.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.types 2 | 3 | import arc.Core 4 | import arc.func.Prov 5 | import arc.graphics.Color 6 | import arc.math.Mathf 7 | import arc.math.geom.Vec2 8 | import arc.scene.Group 9 | import arc.scene.ui.Label 10 | import arc.scene.ui.layout.Scl 11 | import arc.scene.ui.layout.Table 12 | import arc.struct.ObjectMap 13 | import arc.util.* 14 | import mindustry.core.UI 15 | import mindustry.graphics.Pal 16 | import mindustry.ui.Styles 17 | import mindustry.world.meta.StatUnit 18 | import tmi.recipe.Recipe 19 | import tmi.recipe.RecipeItemStack 20 | import tmi.recipe.RecipeType 21 | import tmi.set 22 | import tmi.ui.NODE_SIZE 23 | import tmi.ui.NodeType 24 | import tmi.ui.RecipeNode 25 | import tmi.ui.RecipeView.LineMeta 26 | import tmi.util.Consts 27 | import kotlin.math.max 28 | 29 | open class FactoryRecipe : RecipeType() { 30 | val bound: Vec2 = Vec2() 31 | val blockPos: Vec2 = Vec2() 32 | val consPos: ObjectMap, Vec2> = ObjectMap() 33 | val prodPos: ObjectMap, Vec2> = ObjectMap() 34 | val optPos: Vec2 = Vec2() 35 | 36 | var doubleInput: Boolean = false 37 | var doubleOutput: Boolean = false 38 | var hasOptionals: Boolean = false 39 | var time: Float = 0f 40 | 41 | override val id = 1 42 | 43 | override fun buildView(view: Group) { 44 | val label = Label(Core.bundle["misc.factory"], Styles.outlineLabel) 45 | label.validate() 46 | 47 | label.setPosition(blockPos.x + NODE_SIZE/2 + ITEM_PAD + label.prefWidth/2, blockPos.y, Align.center) 48 | view.addChild(label) 49 | 50 | buildOpts(view) 51 | buildTime(view, label.height) 52 | } 53 | 54 | protected fun buildTime(view: Group?, offY: Float) { 55 | if (time > 0) { 56 | val time = Table{ t -> 57 | t.image(Consts.time).scaling(Scaling.fit).size(24f).pad(4f) 58 | t.add( 59 | (if (this.time > 3600) UI.formatTime(this.time) 60 | else Strings.autoFixed(this.time/60, 2) + StatUnit.seconds.localized()), 61 | Styles.outlineLabel 62 | ) 63 | } 64 | time.validate() 65 | 66 | time.setPosition( 67 | blockPos.x + NODE_SIZE/2 + ITEM_PAD + time.prefWidth/2, 68 | blockPos.y - offY - 4, 69 | Align.center 70 | ) 71 | view!!.addChild(time) 72 | } 73 | } 74 | 75 | protected fun buildOpts(view: Group) { 76 | if (!hasOptionals) return 77 | 78 | val optionals = Label(Core.bundle["misc.optional"], Styles.outlineLabel) 79 | optionals.setColor(Pal.accent) 80 | optionals.validate() 81 | optionals.setPosition(optPos.x, optPos.y - optionals.height, Align.center) 82 | view.addChild(optionals) 83 | } 84 | 85 | override fun initial(recipe: Recipe): Vec2 { 86 | time = recipe.craftTime 87 | 88 | consPos.clear() 89 | prodPos.clear() 90 | optPos.setZero() 91 | blockPos.setZero() 92 | 93 | val mats: List> = 94 | recipe.materials.values().filter { e -> !e.optionalCons } 95 | val opts: List> = 96 | recipe.materials.values().filter { e -> e.optionalCons } 97 | val materialNum = mats.size 98 | val productionNum = recipe.productions.size 99 | hasOptionals = opts.isNotEmpty() 100 | doubleInput = materialNum > DOUBLE_LIMIT 101 | doubleOutput = productionNum > DOUBLE_LIMIT 102 | 103 | bound.setZero() 104 | 105 | var wOpt = 0f 106 | var wMat = 0f 107 | var wProd = 0f 108 | 109 | if (hasOptionals) { 110 | wOpt = handleBound(opts.size, false) 111 | bound.y += ROW_PAD 112 | } 113 | if (materialNum > 0) { 114 | wMat = handleBound(materialNum, doubleInput) 115 | bound.y += ROW_PAD 116 | } 117 | bound.y += NODE_SIZE 118 | if (productionNum > 0) { 119 | bound.y += ROW_PAD 120 | wProd = handleBound(productionNum, doubleOutput) 121 | } 122 | 123 | val offOptX = (bound.x - wOpt)/2 124 | val offMatX = (bound.x - wMat)/2 125 | val offProdX = (bound.x - wProd)/2 126 | 127 | val centX = bound.x/2f 128 | var offY = NODE_SIZE/2 129 | 130 | if (hasOptionals) { 131 | offY = handleNode(opts, consPos, offOptX, offY, isDouble = false, turn = false) 132 | optPos[bound.x/2] = offY 133 | offY += ROW_PAD 134 | } 135 | if (materialNum > 0) { 136 | offY = handleNode(mats, consPos, offMatX, offY, doubleInput, false) 137 | offY += ROW_PAD 138 | } 139 | blockPos[centX] = offY 140 | offY += NODE_SIZE 141 | if (productionNum > 0) { 142 | offY += ROW_PAD 143 | val seq: List> = recipe.productions.values().toList() 144 | handleNode(seq, prodPos, offProdX, offY, doubleOutput, true) 145 | } 146 | 147 | return bound 148 | } 149 | 150 | protected fun handleNode( 151 | seq: List>, 152 | pos: ObjectMap, Vec2>, 153 | offX: Float, 154 | offY: Float, 155 | isDouble: Boolean, 156 | turn: Boolean 157 | ): Float { 158 | var yOff = offY 159 | var dx = NODE_SIZE/2 160 | if (isDouble) { 161 | for (i in seq.indices) { 162 | if (turn) { 163 | if (i%2 == 0) pos[seq[i].item] = Vec2(offX + dx, yOff + NODE_SIZE + ITEM_PAD) 164 | else pos[seq[i].item] = Vec2(offX + dx, yOff) 165 | } 166 | if (i%2 == 0) pos[seq[i].item] = Vec2(offX + dx, yOff) 167 | else pos[seq[i].item] = Vec2(offX + dx, yOff + NODE_SIZE + ITEM_PAD) 168 | 169 | dx += NODE_SIZE/2 + ITEM_PAD 170 | } 171 | yOff += NODE_SIZE*2 + ITEM_PAD 172 | } 173 | else { 174 | for (i in 0 until seq.size) { 175 | pos[seq[i].item] = Vec2(offX + dx, yOff) 176 | dx += NODE_SIZE + ITEM_PAD 177 | } 178 | yOff += NODE_SIZE 179 | } 180 | return yOff 181 | } 182 | 183 | protected fun handleBound(num: Int, isDouble: Boolean): Float { 184 | var res: Float 185 | if (isDouble) { 186 | val n = Mathf.ceil(num/2f) 187 | bound.x = max(bound.x.toDouble(), 188 | (NODE_SIZE*n + 2*ITEM_PAD*(n - 1) + (1 - num%2)*(NODE_SIZE/2 + ITEM_PAD/2)).also { 189 | res = it 190 | } 191 | .toDouble() 192 | ).toFloat() 193 | bound.y += NODE_SIZE*2 + ITEM_PAD 194 | } 195 | else { 196 | bound.x = max( 197 | bound.x.toDouble(), 198 | (NODE_SIZE*num + ITEM_PAD*(num - 1)).also { 199 | res = it 200 | }.toDouble() 201 | ).toFloat() 202 | bound.y += NODE_SIZE 203 | } 204 | 205 | return res 206 | } 207 | 208 | override fun layout(recipeNode: RecipeNode) { 209 | when (recipeNode.type) { 210 | NodeType.MATERIAL -> { 211 | val pos = consPos[recipeNode.stack.item] 212 | recipeNode.setPosition(pos.x, pos.y, Align.center) 213 | } 214 | NodeType.PRODUCTION -> { 215 | val pos = prodPos[recipeNode.stack.item] 216 | recipeNode.setPosition(pos.x, pos.y, Align.center) 217 | } 218 | NodeType.BLOCK -> { 219 | recipeNode.setPosition(blockPos.x, blockPos.y, Align.center) 220 | } 221 | } 222 | } 223 | 224 | override fun line(from: RecipeNode, to: RecipeNode): LineMeta { 225 | val res = LineMeta() 226 | 227 | if (from.stack.optionalCons) return res 228 | 229 | res.color = if (from.type == NodeType.MATERIAL) Prov { 230 | Tmp.c1.set(Color.gray).lerp( 231 | Pal.accent, Mathf.pow( 232 | Mathf.absin( 233 | Time.globalTime/8 + Mathf.pi, 1f, 1f 234 | ), 3f 235 | ) 236 | ) 237 | } 238 | else Prov { 239 | Tmp.c1.set(Color.gray).lerp( 240 | Pal.accent, Mathf.pow( 241 | Mathf.absin( 242 | Time.globalTime/8, 1f, 1f 243 | ), 3f 244 | ) 245 | ) 246 | } 247 | 248 | val offX = from.width/2 249 | val offY = from.height/2 250 | val offX1 = to.width/2 251 | val offY1 = to.height/2 252 | 253 | val off = if ((to.y - from.y) > 0) -ROW_PAD/2 - NODE_SIZE/2 else ROW_PAD/2 + NODE_SIZE/2 254 | if (from.x != to.x) { 255 | res.addVertex(from.x + offX, from.y + offY) 256 | res.addVertex(from.x + offX, to.y + offY1 + off) 257 | res.addVertex(to.x + offX1, to.y + offY1 + off) 258 | res.addVertex(to.x + offX1, to.y + offY1) 259 | } 260 | else { 261 | res.addVertex(from.x + offX, from.y + offY) 262 | res.addVertex(to.x + offX1, to.y + offY1) 263 | } 264 | 265 | return res 266 | } 267 | 268 | companion object { 269 | val ROW_PAD: Float = Scl.scl(60f) 270 | val ITEM_PAD: Float = Scl.scl(10f) 271 | const val DOUBLE_LIMIT: Int = 5 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/types/GeneratorRecipe.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.types 2 | 3 | import arc.Core 4 | import arc.math.geom.Vec2 5 | import arc.scene.Group 6 | import arc.scene.ui.Label 7 | import arc.struct.ObjectSet 8 | import arc.util.Align 9 | import mindustry.ui.Styles 10 | import tmi.recipe.Recipe 11 | import tmi.recipe.RecipeItemStack 12 | import tmi.ui.NODE_SIZE 13 | import tmi.ui.NodeType 14 | import tmi.ui.RecipeNode 15 | import tmi.ui.RecipeView.LineMeta 16 | 17 | class GeneratorRecipe : FactoryRecipe() { 18 | private val powers = ObjectSet>() 19 | 20 | override val id = 3 21 | 22 | fun addPower(item: RecipeItem<*>){ 23 | powers.add(item) 24 | } 25 | 26 | override fun buildView(view: Group) { 27 | val label = Label(Core.bundle["misc.generator"], Styles.outlineLabel) 28 | label.layout() 29 | 30 | label.setPosition( 31 | blockPos.x + NODE_SIZE/2 + ITEM_PAD + label.prefWidth/2, 32 | blockPos.y, 33 | Align.center 34 | ) 35 | view.addChild(label) 36 | 37 | buildOpts(view) 38 | buildTime(view, label.height) 39 | } 40 | 41 | override fun initial(recipe: Recipe): Vec2 { 42 | time = recipe.craftTime 43 | 44 | consPos.clear() 45 | prodPos.clear() 46 | blockPos.setZero() 47 | 48 | val mats = recipe.materials.values().filter { e -> !e.optionalCons } 49 | val opts = recipe.materials.values().filter { e -> e.optionalCons } 50 | val prod = arrayListOf>() 51 | val powers = arrayListOf>() 52 | for (item in recipe.productions.values()) { 53 | if (isPower(item.item)) powers.add(item) 54 | else prod.add(item) 55 | } 56 | 57 | val materialNum = mats.size 58 | val productionNum = prod.size 59 | hasOptionals = opts.isNotEmpty() 60 | doubleInput = materialNum > DOUBLE_LIMIT 61 | doubleOutput = productionNum > DOUBLE_LIMIT 62 | 63 | bound.setZero() 64 | 65 | var wOpt = 0f 66 | var wMat = 0f 67 | var wProd = 0f 68 | var wPow = 0f 69 | 70 | if (hasOptionals) { 71 | wOpt = handleBound(opts.size, false) 72 | bound.y += ROW_PAD 73 | } 74 | if (materialNum > 0) { 75 | wMat = handleBound(materialNum, doubleInput) 76 | bound.y += ROW_PAD 77 | } 78 | bound.y += NODE_SIZE 79 | if (productionNum > 0) { 80 | bound.y += ROW_PAD 81 | wProd = handleBound(productionNum, doubleOutput) 82 | } 83 | if (powers.any()) { 84 | bound.y += ROW_PAD 85 | wPow = handleBound(powers.size, false) 86 | } 87 | 88 | val offOptX = (bound.x - wOpt)/2 89 | val offMatX = (bound.x - wMat)/2 90 | val offProdX = (bound.x - wProd)/2 91 | val offPowX = (bound.x - wPow)/2 92 | 93 | val centX = bound.x/2f 94 | var offY = NODE_SIZE/2 95 | 96 | if (hasOptionals) { 97 | offY = handleNode(opts, consPos, offOptX, offY, isDouble = false, turn = false) 98 | optPos[bound.x/2] = offY 99 | offY += ROW_PAD 100 | } 101 | if (materialNum > 0) { 102 | offY = handleNode(mats, consPos, offMatX, offY, doubleInput, false) 103 | offY += ROW_PAD 104 | } 105 | blockPos[centX] = offY 106 | offY += NODE_SIZE 107 | if (productionNum > 0) { 108 | offY += ROW_PAD 109 | offY = handleNode(prod, prodPos, offProdX, offY, doubleOutput, true) 110 | } 111 | 112 | if (powers.any()) { 113 | offY += ROW_PAD 114 | handleNode(powers, prodPos, offPowX, offY, false, true) 115 | } 116 | 117 | return bound 118 | } 119 | 120 | override fun line(from: RecipeNode, to: RecipeNode): LineMeta { 121 | return if ((isPower(from.stack.item) && from.type == NodeType.PRODUCTION) || (isPower( 122 | to.stack.item 123 | ) && to.type == NodeType.PRODUCTION) 124 | ) LineMeta() 125 | else super.line(from, to) 126 | } 127 | 128 | fun isPower(item: RecipeItem<*>): Boolean { 129 | return powers.contains(item) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/types/HeatMark.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.types 2 | 3 | import arc.graphics.g2d.TextureRegion 4 | import mindustry.gen.Icon 5 | 6 | object HeatMark: SingleItemMark("heat-mark") { 7 | override val icon: TextureRegion get() = Icon.waves.region 8 | override val ordinal: Int get() = 20000 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/types/PowerMark.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.types 2 | 3 | import arc.Core 4 | import arc.graphics.g2d.TextureRegion 5 | import mindustry.gen.Icon 6 | import tmi.TooManyItems 7 | 8 | object PowerMark: SingleItemMark("power-mark") { 9 | override val icon: TextureRegion get() = Icon.power.region 10 | override val ordinal: Int get() = 10000 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/types/RecipeItem.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.types 2 | 3 | import arc.Core 4 | import arc.graphics.g2d.TextureRegion 5 | 6 | abstract class RecipeItem protected constructor(@JvmField val item: T) : Comparable> { 7 | @Suppress("DEPRECATION") open val ordinal: Int get() = ordinal() 8 | @Suppress("DEPRECATION") open val typeOrdinal: Int get() = typeOrdinal() 9 | @Suppress("DEPRECATION") open val typeID: Int get() = typeID() 10 | @Suppress("DEPRECATION") open val name: String get() = name() 11 | @Suppress("DEPRECATION") open val localizedName: String get() = localizedName() 12 | @Suppress("DEPRECATION") open val icon: TextureRegion get() = icon() 13 | @Suppress("DEPRECATION") open val hidden: Boolean get() = hidden() 14 | @Suppress("DEPRECATION") open val hasDetails: Boolean get() = hasDetails() 15 | @Suppress("DEPRECATION") open val locked: Boolean get() = locked() 16 | 17 | @Deprecated("Use property", ReplaceWith("this.ordinal")) open fun ordinal() = -1 18 | @Deprecated("Use property", ReplaceWith("this.typeOrdinal")) open fun typeOrdinal() = -1 19 | @Deprecated("Use property", ReplaceWith("this.typeID")) open fun typeID() = -1 20 | @Deprecated("Use property", ReplaceWith("this.name")) open fun name() = "" 21 | @Deprecated("Use property", ReplaceWith("this.localizedName")) open fun localizedName() = "" 22 | @Deprecated("Use property", ReplaceWith("this.icon")) open fun icon(): TextureRegion = Core.atlas.find("error") 23 | @Deprecated("Use property", ReplaceWith("this.hidden")) open fun hidden() = false 24 | @Deprecated("Use property", ReplaceWith("this.hasDetails")) open fun hasDetails() = false 25 | @Deprecated("Use property", ReplaceWith("this.locked")) open fun locked() = false 26 | 27 | open fun displayDetails() {} 28 | 29 | override fun compareTo(other: RecipeItem<*>): Int { 30 | val n = typeOrdinal.compareTo(other.typeOrdinal) 31 | 32 | if (n == 0) { 33 | return ordinal - other.ordinal 34 | } 35 | 36 | return n 37 | } 38 | 39 | override fun equals(other: Any?): Boolean { 40 | if (this === other) return true 41 | if (other == null || javaClass != other.javaClass) return false 42 | val that = other as RecipeItem<*> 43 | return item == that.item 44 | } 45 | 46 | override fun hashCode(): Int { 47 | return typeID*31 + name.hashCode() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/recipe/types/SingleItemMark.kt: -------------------------------------------------------------------------------- 1 | package tmi.recipe.types 2 | 3 | import arc.Core 4 | import arc.util.Time 5 | import tmi.TooManyItems 6 | import tmi.recipe.RecipeType 7 | 8 | abstract class SingleItemMark(name: String) : RecipeItem(name) { 9 | init { 10 | Time.run(0f){ 11 | TooManyItems.itemsManager.addItemWrap(name, this) 12 | RecipeType.generator.addPower(this) 13 | } 14 | } 15 | 16 | override val ordinal = -1 17 | override val typeOrdinal = -1 18 | override val typeID = -1 19 | override val name = item 20 | override val localizedName: String = Core.bundle["tmi.$item"] 21 | override val hidden = false 22 | } -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/Cursor.kt: -------------------------------------------------------------------------------- 1 | package tmi.ui 2 | 3 | import arc.Graphics 4 | 5 | class Cursor : Graphics.Cursor { 6 | override fun dispose() { 7 | } 8 | 9 | companion object { 10 | var recipe: Graphics.Cursor? = null 11 | var search: Graphics.Cursor? = null 12 | 13 | fun init() { 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/EntryAssigner.kt: -------------------------------------------------------------------------------- 1 | package tmi.ui 2 | 3 | import arc.Core 4 | import arc.func.Boolp 5 | import arc.func.Cons 6 | import arc.func.Prov 7 | import arc.graphics.Color 8 | import arc.input.KeyCode 9 | import arc.scene.event.DragListener 10 | import arc.scene.event.InputEvent 11 | import arc.scene.event.InputListener 12 | import arc.scene.ui.Dialog 13 | import arc.scene.ui.ImageButton 14 | import arc.scene.ui.ScrollPane 15 | import arc.scene.ui.layout.Scl 16 | import arc.scene.ui.layout.Table 17 | import arc.util.Align 18 | import arc.util.Time 19 | import mindustry.Vars 20 | import mindustry.ctype.UnlockableContent 21 | import mindustry.graphics.Pal 22 | import mindustry.ui.Styles 23 | import mindustry.ui.dialogs.ContentInfoDialog 24 | import tmi.TooManyItems 25 | import tmi.invoke 26 | import tmi.util.CombinedKeys 27 | import tmi.util.Consts 28 | 29 | object EntryAssigner { 30 | lateinit var tmiEntry: ImageButton 31 | private set 32 | 33 | fun assign() { 34 | run { //content information entry 35 | Vars.ui.database.buttons.button(Core.bundle["recipes.open"], Consts.tmi, 38f) { 36 | TmiUI.recipesDialog.currentSelect = null 37 | TmiUI.recipesDialog.show() 38 | } 39 | } 40 | 41 | run { //database entry 42 | Vars.ui.content = object : ContentInfoDialog() { 43 | override fun show(content: UnlockableContent) { 44 | super.show(content) 45 | val rec = TooManyItems.itemsManager.getItem(content) 46 | if (!TooManyItems.recipesManager.anyRecipe(rec)) return 47 | 48 | val pane = Vars.ui.content.cont.children[0] 49 | if (pane is ScrollPane) { 50 | val ta = pane.widget as Table 51 | val t = ta.children[0] as Table 52 | 53 | t.button(Consts.tmi, Styles.clearNonei, 38f) { 54 | TmiUI.recipesDialog.show(TooManyItems.itemsManager.getItem(content)) 55 | hide() 56 | }.padLeft(12f).margin(6f) 57 | } 58 | } 59 | } 60 | } 61 | 62 | run { //HUD entry 63 | Core.scene.root.addChild(object : ImageButton(Consts.tmi, Styles.cleari) { 64 | init { 65 | tmiEntry = this 66 | setSize(Scl.scl(60f)) 67 | 68 | visibility = Boolp { 69 | !Core.scene.hasDialog() && Core.settings.getBool("tmi_button", true) 70 | } 71 | 72 | setPosition( 73 | Core.settings.getFloat("tmi_button_x", 0f), 74 | Core.settings.getFloat("tmi_button_y", 0f), 75 | Align.bottomLeft 76 | ) 77 | 78 | addCaptureListener(object : DragListener() { 79 | init { 80 | button = KeyCode.mouseLeft.ordinal 81 | } 82 | 83 | override fun touchDown(event: InputEvent, x: Float, y: Float, pointer: Int, button: KeyCode): Boolean { 84 | return touchDown(event, x, y, pointer, button.ordinal) 85 | } 86 | 87 | override fun drag(event: InputEvent, mx: Float, my: Float, pointer: Int) { 88 | if (Core.app.isMobile && pointer != 0) return 89 | 90 | setPosition(x + mx, y + my, Align.center) 91 | } 92 | 93 | override fun touchUp(event: InputEvent, x: Float, y: Float, pointer: Int, button: KeyCode) { 94 | if (!isDragging) TmiUI.recipesDialog.show() 95 | else { 96 | Core.settings.put("tmi_button_x", tmiEntry.x) 97 | Core.settings.put("tmi_button_y", tmiEntry.y) 98 | } 99 | super.touchUp(event, x, y, pointer, button.ordinal) 100 | } 101 | }) 102 | } 103 | }) 104 | } 105 | 106 | //preview switch 107 | run { 108 | Vars.ui.settings.game.checkPref("tmi_enable_preview", false) 109 | } 110 | } 111 | 112 | private fun createKeybindTable(table: Table, name: String, key: Prov, hotKeyMethod: Cons>, resetMethod: Runnable, isCombine: Boolean = true) { 113 | table.add(name, Color.white).left().padRight(40f).padLeft(8f) 114 | table.label{ key().ifBlank { Core.bundle["misc.requireInput"] } }.color(Pal.accent).left().minWidth(90f).padRight(20f) 115 | 116 | table.button("@settings.rebind", Styles.defaultt) { openDialog(isCombine){ hotKeyMethod(it) } }.width(130f) 117 | table.button("@settings.resetKey", Styles.defaultt) { resetMethod.run() }.width(130f).pad(2f).padLeft(4f) 118 | table.row() 119 | } 120 | 121 | private fun openDialog(isCombine: Boolean, callBack: Cons>) { 122 | val rebindDialog = Dialog() 123 | val res = linkedSetOf() 124 | var show = "" 125 | 126 | rebindDialog.cont.table{ 127 | it.add(Core.bundle["misc.pressAnyKeys".takeIf { isCombine }?:"keybind.press"]) 128 | .color(Pal.accent) 129 | if (!isCombine) return@table 130 | 131 | it.row() 132 | it.label{ show.ifBlank { Core.bundle["misc.requireInput"] } } 133 | .padTop(8f) 134 | } 135 | 136 | rebindDialog.titleTable.cells.first().pad(4f) 137 | 138 | rebindDialog.addListener(object : InputListener() { 139 | override fun touchDown(event: InputEvent, x: Float, y: Float, pointer: Int, button: KeyCode): Boolean { 140 | if (Core.app.isAndroid) { 141 | rebindDialog.hide() 142 | return false 143 | } 144 | 145 | keySet(button) 146 | return true 147 | } 148 | 149 | override fun keyDown(event: InputEvent, keycode: KeyCode): Boolean { 150 | if (keycode == KeyCode.escape) { 151 | rebindDialog.hide() 152 | return false 153 | } 154 | 155 | keySet(keycode) 156 | return true 157 | } 158 | 159 | private fun keySet(button: KeyCode) { 160 | if (!isCombine) { 161 | callBack(arrayOf(button)) 162 | rebindDialog.hide() 163 | return 164 | } 165 | 166 | if (button != KeyCode.enter) { 167 | res.add(button) 168 | show = CombinedKeys.toString(res) 169 | } 170 | else { 171 | callBack(res.toTypedArray()) 172 | rebindDialog.hide() 173 | } 174 | } 175 | 176 | override fun keyUp(event: InputEvent?, keycode: KeyCode?): Boolean { 177 | res.remove(keycode) 178 | show = CombinedKeys.toString(res) 179 | 180 | return true 181 | } 182 | }) 183 | 184 | rebindDialog.show() 185 | Time.runTask(1f) { Core.scene.setScrollFocus(rebindDialog) } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/NodeType.kt: -------------------------------------------------------------------------------- 1 | package tmi.ui 2 | 3 | enum class NodeType { 4 | MATERIAL, 5 | BLOCK, 6 | PRODUCTION 7 | } 8 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/RecipeNode.kt: -------------------------------------------------------------------------------- 1 | package tmi.ui 2 | 3 | import arc.Core 4 | import arc.func.Cons 5 | import arc.func.Cons3 6 | import arc.graphics.Color 7 | import arc.graphics.g2d.Draw 8 | import arc.input.KeyCode 9 | import arc.math.Mathf 10 | import arc.scene.Element 11 | import arc.scene.event.InputEvent 12 | import arc.scene.event.InputListener 13 | import arc.scene.event.Touchable 14 | import arc.scene.ui.Button 15 | import arc.scene.ui.Tooltip 16 | import arc.scene.ui.layout.Scl 17 | import arc.scene.ui.layout.Table 18 | import arc.util.Scaling 19 | import arc.util.Time 20 | import mindustry.Vars 21 | import mindustry.gen.Icon 22 | import mindustry.gen.Sounds.click 23 | import mindustry.gen.Tex 24 | import mindustry.ui.Styles 25 | import tmi.TooManyItems 26 | import tmi.recipe.RecipeItemStack 27 | import tmi.util.Shapes 28 | 29 | val NODE_SIZE: Float = Scl.scl(80f) 30 | 31 | /**在[tmi.recipe.RecipeType]进行布局时所操作的元素对象,用于显示单个条目信息和提供控制逻辑 */ 32 | class RecipeNode( 33 | val type: NodeType, 34 | val stack: RecipeItemStack<*>, 35 | val clickListener: Cons3, NodeType, RecipesDialog.Mode>? = null 36 | ) : Button() { 37 | private var lastTouchedTime = 0f 38 | private var progress: Float = 0f 39 | private var alpha: Float = 0f 40 | private var clicked = 0 41 | var activity: Boolean = false 42 | var touched: Boolean = false 43 | 44 | init { 45 | background = Tex.button 46 | touchable = Touchable.enabled 47 | 48 | defaults().padLeft(8f).padRight(8f) 49 | 50 | setSize(NODE_SIZE) 51 | 52 | addListener(object : Tooltip(Cons { t: Table -> t.add(stack.item.localizedName, Styles.outlineLabel) }) { 53 | init { 54 | allowMobile = true 55 | } 56 | }) 57 | 58 | addListener(object : InputListener() { 59 | override fun enter(event: InputEvent?, x: Float, y: Float, pointer: Int, fromActor: Element?) { 60 | super.enter(event, x, y, pointer, fromActor) 61 | activity = true 62 | } 63 | 64 | override fun exit(event: InputEvent?, x: Float, y: Float, pointer: Int, toActor: Element?) { 65 | super.exit(event, x, y, pointer, toActor) 66 | activity = false 67 | } 68 | 69 | override fun touchDown(event: InputEvent?, x: Float, y: Float, pointer: Int, button: KeyCode?): Boolean { 70 | if (pointer != 0 && button != KeyCode.mouseLeft && button != KeyCode.mouseRight) return false 71 | lastTouchedTime = Time.globalTime 72 | touched = true 73 | activity = true 74 | return true 75 | } 76 | 77 | override fun touchUp(event: InputEvent?, x: Float, y: Float, pointer: Int, button: KeyCode?) { 78 | if (pointer != 0 && button != KeyCode.mouseLeft && button != KeyCode.mouseRight) return 79 | touched = false 80 | activity = false 81 | 82 | if (stack.item.hasDetails && (progress >= 0.95f || click == null)) { 83 | stack.item.displayDetails() 84 | progress = 0f 85 | 86 | return 87 | } 88 | 89 | if (clickListener != null && Time.globalTime - lastTouchedTime < 12) { 90 | if (!Vars.mobile || Core.settings.getBool("keyboard")) { 91 | clickListener.get( 92 | stack, 93 | type, 94 | if (button == KeyCode.mouseRight) 95 | if (type == NodeType.BLOCK) RecipesDialog.Mode.FACTORY 96 | else RecipesDialog.Mode.USAGE 97 | else RecipesDialog.Mode.RECIPE 98 | ) 99 | } 100 | else { 101 | clicked++ 102 | if (clicked >= 2) { 103 | clickListener.get( 104 | stack, 105 | type, 106 | if (type == NodeType.BLOCK) RecipesDialog.Mode.FACTORY 107 | else RecipesDialog.Mode.USAGE 108 | ) 109 | clicked = 0 110 | } 111 | } 112 | } 113 | } 114 | }) 115 | 116 | stack( 117 | Table { it.image(stack.item.icon).size(NODE_SIZE/Scl.scl()*0.62f).scaling(Scaling.fit) }, 118 | 119 | Table { 120 | var last = false 121 | 122 | it.left().bottom() 123 | it.add(stack.getAmount(), Styles.outlineLabel) 124 | .apply { 125 | if (stack.alternativeFormat != null) update{ l -> 126 | val isDown = Core.input.keyDown(TooManyItems.binds.hotKey) 127 | if (last != isDown){ 128 | l.setText( 129 | if (isDown && stack.alternativeFormat != null) stack.alternativeFormat!!.format(stack.amount) 130 | else stack.getAmount() 131 | ) 132 | last = isDown 133 | } 134 | } 135 | } 136 | it.pack() 137 | }, 138 | 139 | Table { 140 | if (!stack.item.locked) return@Table 141 | it.right().bottom().defaults().right().bottom().pad(4f) 142 | it.image(Icon.lock).scaling(Scaling.fit).size(10f).color(Color.lightGray) 143 | } 144 | ).grow().pad(5f) 145 | } 146 | 147 | override fun act(delta: Float) { 148 | super.act(delta) 149 | alpha = Mathf.lerpDelta(alpha, (if (touched || activity) 1 else 0).toFloat(), 0.08f) 150 | progress = Mathf.approachDelta(progress, if (stack.item.hasDetails && click != null && touched) 1f else 0f, 1/60f) 151 | 152 | if (clickListener != null && Time.globalTime - lastTouchedTime > 12 && clicked == 1) { 153 | clickListener.get(stack, type, RecipesDialog.Mode.RECIPE) 154 | clicked = 0 155 | } 156 | } 157 | 158 | override fun drawBackground(x: Float, y: Float) { 159 | super.drawBackground(x, y) 160 | Draw.color(Color.lightGray) 161 | Draw.alpha(0.5f) 162 | 163 | Shapes.fan(x + width/2, y + height/2, Scl.scl(32f), -progress*360, 90f) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/RecipeView.kt: -------------------------------------------------------------------------------- 1 | package tmi.ui 2 | 3 | import arc.func.Cons 4 | import arc.func.Cons3 5 | import arc.func.Prov 6 | import arc.graphics.Color 7 | import arc.graphics.g2d.Draw 8 | import arc.math.geom.Vec2 9 | import arc.scene.Group 10 | import arc.struct.FloatSeq 11 | import arc.struct.Seq 12 | import tmi.recipe.Recipe 13 | import tmi.recipe.RecipeItemStack 14 | 15 | /**配方表显示的布局元素,用于为添加的[RecipeNode]设置正确的位置并将他们显示到界面容器当中 */ 16 | class RecipeView @JvmOverloads constructor( 17 | val recipe: Recipe, 18 | defClicked: Cons3, NodeType, RecipesDialog.Mode>? = null, 19 | nodeListenerBuilder: Cons? = null 20 | ) : Group() { 21 | private val bound = Vec2() 22 | private val nodes = Seq() 23 | 24 | val lines = Seq() 25 | private val backGroup = object : Group() {} 26 | private val childGroup = object : Group() {} 27 | 28 | init { 29 | addChild(backGroup) 30 | 31 | for (content in recipe.materials.values()) { 32 | nodes.add(RecipeNode(NodeType.MATERIAL, content, defClicked)) 33 | } 34 | for (content in recipe.productions.values()) { 35 | nodes.add(RecipeNode(NodeType.PRODUCTION, content, defClicked)) 36 | } 37 | 38 | if (recipe.ownerBlock != null){ 39 | nodes.add(RecipeNode(NodeType.BLOCK, RecipeItemStack(recipe.ownerBlock), defClicked)) 40 | } 41 | 42 | nodes.each { actor -> 43 | this.addChild(actor) 44 | 45 | nodeListenerBuilder?.get(actor) 46 | } 47 | 48 | addChild(childGroup) 49 | } 50 | 51 | override fun layout() { 52 | super.layout() 53 | backGroup.clear() 54 | childGroup.clear() 55 | 56 | lines.clear() 57 | bound.set(recipe.recipeType.initial(recipe)) 58 | 59 | val center = nodes.find { e: RecipeNode -> e.type == NodeType.BLOCK } 60 | for (node in nodes) { 61 | recipe.recipeType.layout(node) 62 | val line = recipe.recipeType.line(node, center) 63 | lines.add(line) 64 | } 65 | 66 | recipe.recipeType.buildView(childGroup) 67 | recipe.recipeType.buildBack(backGroup) 68 | } 69 | 70 | override fun draw() { 71 | validate() 72 | 73 | Draw.alpha(parentAlpha) 74 | recipe.recipeType.drawLine(this) 75 | super.draw() 76 | } 77 | 78 | override fun getPrefWidth(): Float { 79 | return bound.x 80 | } 81 | 82 | override fun getPrefHeight(): Float { 83 | return bound.y 84 | } 85 | 86 | class LineMeta { 87 | val vertices: FloatSeq = FloatSeq() 88 | var color: Prov = Prov { Color.white } 89 | 90 | fun setVertices(vararg vert: Float) { 91 | vertices.clear() 92 | vertices.addAll(*vert) 93 | } 94 | 95 | fun addVertex(x: Float, y: Float) { 96 | vertices.add(x, y) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/Side.kt: -------------------------------------------------------------------------------- 1 | package tmi.ui 2 | 3 | import arc.scene.style.Drawable 4 | import tmi.util.Consts 5 | 6 | enum class Side(val drawable: Drawable) { 7 | LEFT(Consts.side_left), 8 | RIGHT(Consts.side_right), 9 | TOP(Consts.side_top), 10 | BOTTOM(Consts.side_bottom) 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/designer/BalanceStatistic.kt: -------------------------------------------------------------------------------- 1 | package tmi.ui.designer 2 | 3 | import arc.math.Mathf 4 | import arc.struct.* 5 | import tmi.TooManyItems 6 | import tmi.forEach 7 | import tmi.recipe.Recipe 8 | import tmi.recipe.RecipeItemStack 9 | import tmi.recipe.types.RecipeItem 10 | import tmi.set 11 | 12 | class BalanceStatistic(private val ownerView: DesignerView) { 13 | companion object{ 14 | private const val ZERO = 0.0001f 15 | } 16 | 17 | private val allRecipes = ObjectIntMap() 18 | 19 | private val inputTypes = ObjectSet>() 20 | private val outputTypes = ObjectSet>() 21 | 22 | private val inputs = OrderedMap, RecipeItemStack<*>>() 23 | private val outputs = OrderedMap, RecipeItemStack<*>>() 24 | private val redundant = OrderedMap, RecipeItemStack<*>>() 25 | private val missing = OrderedMap, RecipeItemStack<*>>() 26 | 27 | private val missingIndex = ObjectMap, OrderedSet>() 28 | private val redundantIndex = ObjectMap, OrderedSet>() 29 | 30 | private val globalOutputs = OrderedMap, RecipeItemStack<*>>() 31 | private val globalInputs = OrderedMap, RecipeItemStack<*>>() 32 | 33 | private val buildMaterials = OrderedMap, RecipeItemStack<*>>() 34 | 35 | fun reset(){ 36 | allRecipes.clear() 37 | inputs.clear() 38 | outputs.clear() 39 | redundant.clear() 40 | missing.clear() 41 | 42 | missingIndex.clear() 43 | redundantIndex.clear() 44 | 45 | inputTypes.clear() 46 | outputTypes.clear() 47 | 48 | globalInputs.values().forEach { it.amount = 0f } 49 | globalOutputs.values().forEach { it.amount = 0f } 50 | 51 | buildMaterials.clear() 52 | } 53 | 54 | fun setGlobal(input: Iterable>, output: Iterable>) { 55 | globalInputs.clear() 56 | globalOutputs.clear() 57 | input.forEach { globalInputs[it] = RecipeItemStack(it, 0f).unitTimedFormat() } 58 | output.forEach { globalOutputs[it] = RecipeItemStack(it, 0f).unitTimedFormat() } 59 | } 60 | 61 | fun updateStatistic() { 62 | data class CardIO( 63 | val card: Card, 64 | val inputs: List>, 65 | val outputs: List>, 66 | val realIn: List>, 67 | val realOut: List>, 68 | ){ 69 | inline fun eachIn(block: (RecipeItemStack<*>, RecipeItemStack<*>) -> Unit) { 70 | for (i in inputs.indices) { 71 | block(inputs[i], realIn[i]) 72 | } 73 | } 74 | 75 | inline fun eachOut(block: (RecipeItemStack<*>, RecipeItemStack<*>) -> Unit) { 76 | for (i in outputs.indices) { 77 | block(outputs[i], realOut[i]) 78 | } 79 | } 80 | } 81 | 82 | val allCards = (ownerView.cards + ownerView.foldCards).filter { it.balanceValid } 83 | val cardIOs = allCards.map { card -> 84 | val input = card.inputs() 85 | val output = card.outputs() 86 | 87 | val realIn = input.map l@{ s -> 88 | val linker = card.linkerIns.find { it.item == s.item } 89 | if (linker == null || !linker.isNormalized) return@l RecipeItemStack(s.item, 0f) 90 | 91 | var expect = 0f 92 | linker.links.forEach { l, e -> 93 | expect += (if (linker.links.size == 1) 1f else e.rate)*l.expectAmount 94 | } 95 | 96 | RecipeItemStack(linker.item, expect) 97 | } 98 | val realOut = output.map l@{ s -> 99 | val linker = card.linkerOuts.find { it.item == s.item } 100 | if (linker == null) return@l RecipeItemStack(s.item, 0f) 101 | 102 | var expect = 0f 103 | linker.links.forEach { l, _ -> 104 | if (!l.isNormalized) return@forEach 105 | val rate = if (l.links.size == 1) 1f else l.links[linker].rate 106 | expect += rate*l.expectAmount 107 | } 108 | 109 | RecipeItemStack(linker.item, expect) 110 | } 111 | CardIO(card, input, output, realIn, realOut) 112 | } 113 | 114 | val recipeCards = allCards.filterIsInstance().filter { it.balanceValid } 115 | val inputCards = allCards.filterIsInstance().filter { it.isInput } 116 | val outputCards = allCards.filterIsInstance().filter { !it.isInput } 117 | 118 | recipeCards.forEach { c -> 119 | allRecipes.put(c.recipe, c.mul) 120 | 121 | TooManyItems.recipesManager 122 | .getRecipesByBuilding(c.recipe.ownerBlock as RecipeItem<*>) 123 | .firstOpt()?.let { rec -> 124 | rec.materials.values().forEach { 125 | val stack = buildMaterials.get(it.item) { 126 | RecipeItemStack(it.item, 0f).integerFormat() 127 | } 128 | 129 | stack.amount += it.amount * rec.craftTime 130 | } 131 | } 132 | } 133 | 134 | inputCards.forEach { 135 | it.outputs()/* card input means linker output */.forEach { s -> 136 | inputTypes.add(s.item) 137 | val stack = inputs.get(s.item) { 138 | RecipeItemStack(s.item, 0f).unitTimedFormat() 139 | } 140 | 141 | stack.amount += s.amount 142 | } 143 | } 144 | outputCards.forEach { 145 | it.inputs()/* card output means linker input */.forEach { s -> 146 | outputTypes.add(s.item) 147 | val stack = outputs.get(s.item) { 148 | RecipeItemStack(s.item, 0f).unitTimedFormat() 149 | } 150 | 151 | stack.amount += s.amount 152 | } 153 | } 154 | 155 | cardIOs.forEach { io -> 156 | io.eachOut { s, rs -> 157 | outputTypes.add(s.item) 158 | var red = globalOutputs[s.item] 159 | 160 | if (red == null && s.amount - rs.amount > ZERO){ 161 | redundantIndex.get(s.item) { OrderedSet() }.add(io.card) 162 | } 163 | 164 | red = red?:redundant.get(s.item){ 165 | RecipeItemStack(s.item, 0f).unitTimedFormat() 166 | } 167 | red.amount += Mathf.maxZero(s.amount - rs.amount) 168 | } 169 | 170 | io.eachIn { s, rs -> 171 | inputTypes.add(s.item) 172 | var mis = globalInputs[s.item] 173 | 174 | if (mis == null && s.amount - rs.amount > ZERO){ 175 | missingIndex.get(s.item) { OrderedSet() }.add(io.card) 176 | } 177 | 178 | mis = mis?:missing.get(s.item) { 179 | RecipeItemStack(s.item, 0f).unitTimedFormat() 180 | } 181 | mis.amount += Mathf.maxZero(s.amount - rs.amount) 182 | } 183 | } 184 | } 185 | 186 | fun inputTypes() = inputTypes.toSet() 187 | fun outputTypes() = outputTypes.toSet() 188 | 189 | fun resultInputs() = inputs.values().filter { it.amount > ZERO } 190 | fun resultOutputs() = outputs.values().filter { it.amount > ZERO } 191 | fun resultMissing() = missing.values().filter { it.amount > ZERO }.sortedBy { -it.amount } 192 | fun resultRedundant() = redundant.values().filter { it.amount > ZERO }.sortedBy { -it.amount } 193 | fun resultGlobalInputs() = globalInputs.values().filter { it.amount > ZERO } 194 | fun resultGlobalOutputs() = globalOutputs.values().filter { it.amount > ZERO } 195 | fun resultBuildMaterials() = buildMaterials.values().filter { it.amount > ZERO } 196 | 197 | fun resultMissingIndex(item: RecipeItem<*>) = missingIndex[item].toList() 198 | fun resultRedundantIndex(item: RecipeItem<*>) = redundantIndex[item].toList() 199 | 200 | @Suppress("UNCHECKED_CAST") 201 | fun allBlocks(): List> = allRecipes.map { 202 | RecipeItemStack(it.key.ownerBlock as RecipeItem, it.value.toFloat()).integerFormat() 203 | } 204 | 205 | override fun toString(): String { 206 | val res = StringBuilder() 207 | 208 | inputs.values().filter { it.amount > ZERO }.also { 209 | if (it.isEmpty()) return@also 210 | res.append("inputs:\n") 211 | it.forEach{ e -> res.append("| ${e.item.name}: ${e.amount}\n") } 212 | } 213 | outputs.values().filter { it.amount > ZERO }.also { 214 | if (it.isEmpty()) return@also 215 | res.append("outputs:\n") 216 | it.forEach{ e -> res.append("| ${e.item.name}: ${e.amount}\n") } 217 | } 218 | 219 | globalInputs.values().filter { it.amount > ZERO }.also { 220 | if (it.isEmpty()) return@also 221 | res.append("global inputs:\n") 222 | it.forEach{ e -> res.append("| ${e.item.name}: ${e.amount}\n") } 223 | } 224 | globalOutputs.values().filter { it.amount > ZERO }.also { 225 | if (it.isEmpty()) return@also 226 | res.append("global outputs:\n") 227 | it.forEach{ e -> res.append("| ${e.item.name}: ${e.amount}\n") } 228 | } 229 | 230 | redundant.values().filter { it.amount > ZERO }.also { 231 | if (it.isEmpty()) return@also 232 | res.append("redundant:\n") 233 | it.forEach{ e -> res.append("| ${e.item.name}: ${e.amount}\n") } 234 | } 235 | missing.values().filter { it.amount > ZERO }.also { 236 | if (it.isEmpty()) return@also 237 | res.append("missing:\n") 238 | it.forEach{ e -> res.append("| ${e.item.name}: ${e.amount}\n") } 239 | } 240 | 241 | return res.toString() 242 | } 243 | } -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/designer/Events.kt: -------------------------------------------------------------------------------- 1 | package tmi.ui.designer 2 | 3 | import arc.scene.event.SceneEvent 4 | 5 | class UndoEvent(val handle: DesignerHandle) 6 | class RedoEvent(val handle: DesignerHandle) -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/designer/FoldLink.kt: -------------------------------------------------------------------------------- 1 | package tmi.ui.designer 2 | 3 | import arc.graphics.Color 4 | import arc.graphics.g2d.Draw 5 | import arc.graphics.g2d.Fill 6 | import arc.graphics.g2d.Lines 7 | import arc.math.geom.Vec2 8 | import arc.scene.Element 9 | import arc.scene.ui.layout.Scl 10 | import arc.util.Align 11 | import mindustry.gen.Icon 12 | import mindustry.graphics.Pal 13 | import tmi.util.drawable1 14 | 15 | open class FoldLink( 16 | val card: Card, 17 | val linker: ItemLinker?, 18 | val inFold: Boolean 19 | ): Element() { 20 | private var _centerUpdated = false 21 | val centerPos = Vec2() 22 | get() = field 23 | .takeIf { _centerUpdated }?:localToAscendantCoordinates(card.ownerDesigner, field.set(width/2, height/2)) 24 | .also { _centerUpdated = true } 25 | val linesColor: Color = Pal.accent.cpy() 26 | 27 | override fun act(delta: Float) { 28 | super.act(delta) 29 | _centerUpdated = false 30 | } 31 | 32 | override fun draw() { 33 | super.draw() 34 | 35 | Draw.color(card.backColor) 36 | Fill.circle(getX(Align.center), getY(Align.center), width/2 + Scl.scl(6f)) 37 | Lines.stroke(Scl.scl(4f), card.foldColor) 38 | Lines.circle(getX(Align.center), getY(Align.center), width/2) 39 | 40 | val icon = (card.foldIcon?: when(card){ 41 | is RecipeCard -> drawable1.set(card.recipe.ownerBlock?.icon).takeIf { it.region != null }?: Icon.layers 42 | is IOCard -> if (card.isInput) Icon.download else Icon.upload 43 | else -> Icon.layers 44 | }) 45 | val isFont = icon::class.java.name.contains("Fonts") 46 | Draw.color(if (!isFont) Color.white else card.iconColor) 47 | icon.draw( 48 | getX(Align.center) - width/2f*0.7f, getY(Align.center) - height/2f*0.7f, 49 | width/2f*0.7f, height/2f*0.7f, 50 | width*(0.7f.takeIf { !isFont }?:1f), height*(0.7f.takeIf { !isFont }?:1f), 51 | 0.7f, 0.7f, 52 | 0f 53 | ) 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/designer/IOCard.kt: -------------------------------------------------------------------------------- 1 | package tmi.ui.designer 2 | 3 | import arc.Core 4 | import arc.graphics.Color 5 | import arc.scene.Element 6 | import arc.scene.ui.Button 7 | import arc.scene.ui.TextField 8 | import arc.scene.ui.layout.Table 9 | import arc.util.Align 10 | import arc.util.Scaling 11 | import arc.util.Strings 12 | import arc.util.io.Reads 13 | import arc.util.io.Writes 14 | import mindustry.core.UI 15 | import mindustry.gen.Icon 16 | import mindustry.ui.Styles 17 | import tmi.TooManyItems 18 | import tmi.forEach 19 | import tmi.recipe.RecipeItemStack 20 | import tmi.recipe.types.RecipeItem 21 | import tmi.ui.TmiUI 22 | import tmi.util.Consts 23 | import tmi.util.Utils 24 | 25 | class IOCard( 26 | ownerDesigner: DesignerView, 27 | val isInput: Boolean 28 | ) : Card(ownerDesigner) { 29 | private val items = sortedMapOf, RecipeItemStack<*>>() 30 | private val inner = Table() 31 | 32 | override val balanceValid: Boolean get() = true 33 | 34 | override fun act(delta: Float) { 35 | for (linker in linkerIns) { 36 | val stack = items[linker.item] ?: continue 37 | 38 | linker.expectAmount = stack.amount 39 | } 40 | 41 | for (linker in linkerOuts) { 42 | val stack = items[linker.item] ?: continue 43 | 44 | linker.expectAmount = stack.amount 45 | } 46 | 47 | super.act(delta) 48 | } 49 | 50 | override fun buildCard(inner: Table) { 51 | inner.table { m -> 52 | m.image(if (isInput) Icon.download else Icon.upload).scaling(Scaling.fit).size(26f).padRight(6f) 53 | m.add(Core.bundle[if (isInput) "dialog.calculator.input" else "dialog.calculator.output"]).growX() 54 | .labelAlign(Align.left).pad(12f) 55 | } 56 | inner.row() 57 | inner.add(this.inner).center().grow().pad(24f).padTop(24f) 58 | 59 | if (isInput) { 60 | inner.row() 61 | inner.button(Core.bundle["dialog.calculator.addItem"], Icon.addSmall, Styles.cleart, 24f) { 62 | ownerDesigner.parentDialog.showMenu(this.inner, Align.bottom, Align.top) { list -> 63 | list.table(Consts.darkGrayUIAlpha) { items -> 64 | val l = TooManyItems.itemsManager.list 65 | .removeAll { e -> !TooManyItems.recipesManager.anyMaterial(e) } 66 | TmiUI.buildItems(items, l, { i, b -> b.setDisabled { this.items.containsKey(i) } }) { item -> 67 | ownerDesigner.pushHandle(IOCardItemHandle(ownerDesigner, this, item, false)) 68 | ownerDesigner.parentDialog.hideMenu() 69 | } 70 | }.margin(8f) 71 | } 72 | }.growX().fillY().margin(8f).marginTop(10f).marginBottom(10f).get().visible { !ownerDesigner.imageGenerating } 73 | } 74 | 75 | buildInner() 76 | } 77 | 78 | override fun buildSimpleCard(inner: Table) { 79 | 80 | } 81 | 82 | private fun buildInner() { 83 | inner.clearChildren() 84 | 85 | var tab: Table? = null 86 | if (items.isEmpty()){ 87 | inner.add(Core.bundle["dialog.calculator.noItems"]).pad(24f).padTop(32f).padBottom(32f) 88 | } 89 | else { 90 | items.values.forEachIndexed { i, item -> 91 | if (i%5 == 0) { 92 | tab = inner.table().growY().fillX().left().pad(5f).get().top() 93 | } 94 | 95 | val button = Button(Styles.cleart).also { t -> 96 | if (isInput) { 97 | t.button(Icon.cancelSmall, Styles.clearNonei, 18f) { 98 | ownerDesigner.pushHandle( 99 | linkerOuts.find { it.item == item.item }?.let { 100 | CombinedHandles(ownerDesigner, 101 | RemoveLinkerHandle(ownerDesigner, it), 102 | IOCardItemHandle(ownerDesigner, this, item.item, true), 103 | ) 104 | }?: IOCardItemHandle(ownerDesigner, this, item.item, true) 105 | ) 106 | }.margin(4f).get().visible { !ownerDesigner.imageGenerating } 107 | } 108 | t.image(item.item.icon).scaling(Scaling.fit).size(42f).pad(4f) 109 | t.add(item.item.localizedName).growX().left().pad(5f) 110 | t.add("").left().update { 111 | val (value, unit) = Utils.unitTimed(item.amount) 112 | 113 | it.setText( 114 | (if (value <= 0) "--" 115 | else if (value > 1000) UI.formatAmount(value.toLong()) 116 | else Strings.autoFixed(value, 2)) 117 | + unit 118 | ) 119 | }.color(Color.lightGray).pad(5f) 120 | } 121 | 122 | if (isInput) { 123 | setNodeMoveLinkerListener(button, item.item, ownerDesigner) 124 | } 125 | else { 126 | button.clicked{ 127 | ownerDesigner.parentDialog.showMenu(button, Align.bottomLeft, Align.topLeft) { pane -> 128 | pane.table(Consts.darkGrayUIAlpha){ 129 | it.add(Core.bundle["dialog.calculator.setAmount"]) 130 | it.row() 131 | it.table{ t -> 132 | var (_, unit) = Utils.unitTimed(item.amount) 133 | 134 | val field = t.field(Strings.autoFixed(item.amount*unit.multi, 2), TextField.TextFieldFilter.floatsOnly){ str -> 135 | try { 136 | item.amount = str.toFloat()/unit.multi 137 | observeUpdate() 138 | } catch (ignored: NumberFormatException){} 139 | }.get() 140 | t.button({ b -> b.add("").pad(4f).update{ l -> l.setText(unit.toString()) } }, Styles.flatt){ 141 | unit = unit.next() 142 | field.text = Strings.autoFixed(item.amount*unit.multi, 2) 143 | }.left().padLeft(4f).color(Color.lightGray).fillX().growY() 144 | } 145 | }.margin(6f) 146 | } 147 | } 148 | } 149 | 150 | tab!!.table(Consts.darkGrayUIAlpha){ it.add(button).left().growX() }.height(60f).growX().left().margin(6f) 151 | tab!!.row() 152 | } 153 | } 154 | 155 | pack() 156 | } 157 | 158 | override fun addIn(linker: ItemLinker) { 159 | super.addIn(linker) 160 | if (!isInput) { 161 | addItem(linker.item) 162 | } 163 | } 164 | 165 | override fun addOut(linker: ItemLinker) { 166 | super.addOut(linker) 167 | if (isInput && !items.containsKey(linker.item)) { 168 | addItem(linker.item) 169 | } 170 | } 171 | 172 | @Suppress("UNCHECKED_CAST") 173 | fun addItem(item: RecipeItem): RecipeItemStack { 174 | var stack = items[item] 175 | if (stack != null) return stack as RecipeItemStack 176 | 177 | stack = RecipeItemStack(item, 0f) 178 | this.items[item] = stack 179 | buildInner() 180 | 181 | return stack 182 | } 183 | 184 | fun removeItem(item: RecipeItem<*>) { 185 | items.remove(item) 186 | buildInner() 187 | } 188 | 189 | override fun removeChild(element: Element, unfocus: Boolean): Boolean { 190 | val b = super.removeChild(element, unfocus) 191 | if (b && !isInput && element is ItemLinker) { 192 | removeItem(element.item) 193 | return true 194 | } 195 | 196 | return false 197 | } 198 | @Deprecated( 199 | message = "unnamed to inputs()", 200 | replaceWith = ReplaceWith("inputs()"), 201 | level = DeprecationLevel.WARNING 202 | ) 203 | override fun accepts() = inputs() 204 | 205 | override fun inputTypes() = if (!isInput) items.values.map { it.item } else emptyList() 206 | override fun outputTypes() = if (isInput) items.values.map { it.item } else emptyList() 207 | override fun inputs() = if (!isInput) items.values.toList() else emptyList() 208 | override fun outputs() = if (isInput) items.values.toList() else emptyList() 209 | 210 | override fun added() {} 211 | 212 | override fun calculateBalance() { 213 | if (!isInput) return 214 | 215 | items.forEach { entry -> 216 | val linker = linkerOuts.find { it.item == entry.key } 217 | var amount = 0f 218 | 219 | linker?.links?.forEach lns@{ other, ent -> 220 | if (!other.isNormalized) return@lns 221 | 222 | amount += (if (other.links.size == 1) 1f else ent.rate)*other.expectAmount 223 | } 224 | 225 | entry.value.amount = amount 226 | } 227 | 228 | for (linker in linkerOuts) { 229 | val stack = items[linker.item] ?: continue 230 | 231 | linker.expectAmount = stack.amount 232 | } 233 | } 234 | 235 | override fun checkLinking(linker: ItemLinker): Boolean { 236 | return !isInput 237 | } 238 | 239 | override fun copy(): IOCard { 240 | val res = IOCard(ownerDesigner, isInput) 241 | res.setBounds(x, y, width, height) 242 | 243 | return res 244 | } 245 | 246 | override fun write(write: Writes) { 247 | write.i(CLASS_ID) 248 | write.bool(isInput) 249 | write.bool(isFold) 250 | 251 | super.write(write) 252 | 253 | write.i(items.size) 254 | items.values.forEach { 255 | write.str(it.item.name) 256 | write.f(it.amount) 257 | } 258 | } 259 | 260 | override fun read(read: Reads, ver: Int) { 261 | super.read(read, ver) 262 | 263 | val n = read.i() 264 | for (i in 0 until n) { 265 | val item = TooManyItems.itemsManager.getByName(read.str()) 266 | val amount = read.f() 267 | addItem(item).amount = amount 268 | } 269 | 270 | observeUpdate() 271 | } 272 | 273 | companion object { 274 | const val CLASS_ID = 2117128239 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/designer/PieChartSetter.kt: -------------------------------------------------------------------------------- 1 | package tmi.ui.designer 2 | 3 | import arc.func.Cons 4 | import arc.func.Cons2 5 | import arc.graphics.Color 6 | import arc.graphics.g2d.Draw 7 | import arc.graphics.g2d.Lines 8 | import arc.input.KeyCode 9 | import arc.math.Angles 10 | import arc.math.Mathf 11 | import arc.scene.Element 12 | import arc.scene.Group 13 | import arc.scene.event.InputEvent 14 | import arc.scene.event.InputListener 15 | import arc.scene.style.Drawable 16 | import arc.scene.ui.layout.Scl 17 | import arc.scene.ui.layout.Table 18 | import arc.util.Align 19 | import mindustry.graphics.Pal 20 | import mindustry.ui.Fonts 21 | import tmi.util.Consts 22 | import tmi.util.Shapes 23 | import tmi.util.vec1 24 | import tmi.util.vec2 25 | import kotlin.math.roundToInt 26 | 27 | class PieChartSetter( 28 | proportionEntries: List>, 29 | private val callback: Cons>>, 30 | back: Drawable? = null, 31 | private val colorList: List = listOf( 32 | Pal.accent, 33 | Pal.heal, 34 | Pal.place, 35 | Pal.remove, 36 | Pal.reactorPurple, 37 | Pal.gray, 38 | ), 39 | private val colorSetter: Cons2? = null, 40 | ): Table(back){ 41 | private var externUpdated = false 42 | private val proportionsData = proportionEntries.map { ProportionData(it.first, it.second, 90f, 0f) } 43 | 44 | init { 45 | normalize() 46 | assignmentAngles() 47 | 48 | table{ pie -> 49 | pie.add(object : Group(){ 50 | override fun draw() { 51 | proportionsData.forEachIndexed { i, it -> 52 | Draw.color(colorList[i % colorList.size]) 53 | Shapes.fan( 54 | getX(Align.center), 55 | getY(Align.center), 56 | width/2 - Scl.scl(12f), 57 | it.angleStep, 58 | it.angle, 59 | ) 60 | Draw.reset() 61 | } 62 | 63 | proportionsData.forEach { 64 | Fonts.outline.draw( 65 | "${(it.angleStep/3.60f).roundToInt()}%", 66 | getX(Align.center) + Angles.trnsx(it.angle + it.angleStep/2, getWidth()/4), 67 | getY(Align.center) + Angles.trnsy(it.angle + it.angleStep/2, getWidth()/4), 68 | Color.white, 69 | Scl.scl(0.5f), 70 | false, 71 | Align.center 72 | ) 73 | } 74 | 75 | super.draw() 76 | } 77 | }.apply par@{ 78 | proportionsData.forEachIndexed { i, t -> 79 | var hover = false 80 | var touched = false 81 | 82 | addChild(object : Element(){ 83 | var dX = 0f 84 | var dY = 0f 85 | 86 | override fun draw() { 87 | super.draw() 88 | 89 | Lines.stroke(2f, Color.white) 90 | Lines.line( 91 | getX(Align.center), 92 | getY(Align.center), 93 | getX(Align.center) - dX, 94 | getY(Align.center) - dY 95 | ) 96 | 97 | Draw.color(if (touched) Color.lightGray else if (hover) Pal.accent else Color.white) 98 | val ang = Angles.angle( 99 | getX(Align.center), 100 | getY(Align.center), 101 | getX(Align.center) - dX, 102 | getY(Align.center) - dY 103 | ) 104 | 105 | Consts.panner.draw( 106 | x, y, width/2, height/2, 107 | width, height, 1f, 1f, 108 | ang 109 | ) 110 | } 111 | 112 | override fun act(delta: Float) { 113 | super.act(delta) 114 | dX = Angles.trnsx(t.angle, this@par.width/2 - 6) 115 | dY = Angles.trnsy(t.angle, this@par.width/2 - 6) 116 | setPosition( 117 | this@par.getX(Align.center) + dX, 118 | this@par.getY(Align.center) + dY, 119 | Align.center 120 | ) 121 | } 122 | }.apply { 123 | t.element = this 124 | 125 | setSize(36f) 126 | addListener(object : InputListener(){ 127 | override fun enter(event: InputEvent?, x: Float, y: Float, pointer: Int, fromActor: Element?) { 128 | hover = true 129 | } 130 | 131 | override fun exit(event: InputEvent?, x: Float, y: Float, pointer: Int, toActor: Element?) { 132 | hover = false 133 | } 134 | 135 | override fun touchDown(event: InputEvent?, x: Float, y: Float, pointer: Int, button: KeyCode?): Boolean { 136 | touched = true 137 | return true 138 | } 139 | 140 | override fun touchUp(event: InputEvent?, x: Float, y: Float, pointer: Int, button: KeyCode?) { 141 | touched = false 142 | computeProportion() 143 | } 144 | 145 | override fun touchDragged(event: InputEvent?, x: Float, y: Float, pointer: Int) { 146 | vec1.set(x, y) 147 | vec2.set(this@par.getX(Align.center), this@par.getY(Align.center)) 148 | localToParentCoordinates(vec1) 149 | vec1.sub(vec2) 150 | 151 | moveAnchor(i, vec1.angle()) 152 | } 153 | }) 154 | }) 155 | } 156 | }).grow() 157 | }.size(220f) 158 | } 159 | 160 | override fun act(delta: Float) { 161 | super.act(delta) 162 | 163 | if (externUpdated){ 164 | normalize() 165 | assignmentAngles() 166 | computeProportion() 167 | 168 | externUpdated = false 169 | } 170 | 171 | proportionsData.forEachIndexed { i, it -> 172 | val color = colorList[i % colorList.size] 173 | colorSetter?.get(it.obj, color) 174 | } 175 | } 176 | 177 | fun set(obj: T, value: Float){ 178 | proportionsData.find { it.obj == obj }?.proporition = value 179 | externUpdated = true 180 | } 181 | 182 | fun average(){ 183 | proportionsData.forEach { it.proporition = 1f/proportionsData.size } 184 | externUpdated = true 185 | } 186 | 187 | private fun moveAnchor(anchorIndex: Int, toAngle: Float){ 188 | val l = proportionsData[Mathf.mod(anchorIndex - 1, proportionsData.size)] 189 | val s = proportionsData[anchorIndex] 190 | val n = proportionsData[Mathf.mod(anchorIndex + 1, proportionsData.size)] 191 | 192 | if (toAngle > n.angle && toAngle < l.angle) return 193 | 194 | s.angle = Mathf.mod(toAngle, 360f) 195 | s.angleStep = Mathf.mod(n.angle - s.angle, 360f) 196 | l.angleStep = Mathf.mod(s.angle - l.angle, 360f) 197 | } 198 | 199 | private fun normalize() { 200 | val pros = proportionsData.associate { it.obj to it.proporition } 201 | 202 | val total = pros.values.sum() 203 | proportionsData.forEach { it.proporition = (pros[it.obj] ?: (1f/proportionsData.size))/total } 204 | } 205 | 206 | private fun assignmentAngles(){ 207 | proportionsData.first().angle = Mathf.mod(proportionsData.first().angle, 360f) 208 | var off = proportionsData.first().angle 209 | proportionsData.forEach { d -> 210 | d.angle = off 211 | d.angleStep = d.proporition * 360f 212 | off += d.angleStep 213 | } 214 | } 215 | 216 | private fun computeProportion(){ 217 | proportionsData.forEach { 218 | val v = it.angleStep/360f 219 | it.proporition = v 220 | } 221 | 222 | callback.get(proportionsData.map { it.obj to it.proporition }) 223 | } 224 | 225 | private inner class ProportionData( 226 | val obj: T, 227 | var proporition: Float, 228 | var angle: Float, 229 | var angleStep: Float 230 | ){ 231 | var element: Element? = null 232 | } 233 | } 234 | 235 | 236 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/designer/SchematicGraphDrawer.kt: -------------------------------------------------------------------------------- 1 | package tmi.ui.designer 2 | 3 | import arc.Core 4 | import arc.graphics.Camera 5 | import arc.graphics.Color 6 | import arc.graphics.g2d.Draw 7 | import arc.graphics.g2d.Lines 8 | import arc.graphics.gl.FrameBuffer 9 | import arc.math.geom.Vec2 10 | import arc.scene.Element 11 | import arc.scene.ui.layout.Scl 12 | import arc.scene.ui.layout.Table 13 | import arc.util.Tmp 14 | import mindustry.graphics.Pal 15 | import tmi.ui.Side 16 | 17 | object SchematicGraphDrawer { 18 | fun drawFoldPane( 19 | view: DesignerView, 20 | buff: FrameBuffer, 21 | scl: Float, 22 | foldedSide: Side, 23 | cardScl: Float, 24 | padding: Float, 25 | targetWidth: Float, 26 | targetHeight: Float 27 | ){ 28 | val pane = buildFoldedPane(view, foldedSide, padding, cardScl, targetWidth, targetHeight) 29 | val size = Vec2(pane.width, pane.height) 30 | 31 | buff.resize((size.x*scl).toInt(), (size.y*scl).toInt()) 32 | buff.begin(Pal.darkestGray) 33 | val camera = Camera() 34 | camera.width = size.x 35 | camera.height = size.y 36 | camera.position.x = size.x/2f 37 | camera.position.y = size.y/2f 38 | camera.update() 39 | Draw.proj(camera) 40 | 41 | pane.setPosition(0f, 0f) 42 | pane.draw() 43 | Lines.stroke(Scl.scl(2f), Color.black) 44 | Lines.quad( 45 | 0f, 0f, 46 | size.x, 0f, 47 | size.x, size.y, 48 | 0f, size.y 49 | ) 50 | 51 | Draw.proj() 52 | buff.end() 53 | } 54 | 55 | private fun buildFoldedPane( 56 | view: DesignerView, 57 | foldedSide: Side, 58 | padding: Float, 59 | cardScl: Float, 60 | targetWidth: Float, 61 | targetHeight: Float 62 | ): Table { 63 | val res = Table() 64 | if (view.foldCards.isEmpty) return res 65 | 66 | res.add(Core.bundle["dialog.calculator.foldedCards"]).pad(16f).growX().fillY() 67 | res.row() 68 | res.image().color(Color.black).growX().height(4f).padBottom(8f) 69 | res.row() 70 | 71 | val list = mutableListOf() 72 | if (foldedSide == Side.LEFT || foldedSide == Side.RIGHT) { 73 | val cont = res.table().fillX().growY().get().top() 74 | var table = Table() 75 | view.foldCards.forEach { card -> 76 | val elem = buildCardShower(view, card, foldedSide, cardScl, padding) 77 | 78 | if (table.prefHeight + elem.prefHeight > targetHeight) { 79 | list.add(table) 80 | table = Table() 81 | } 82 | table.center().add(elem).fillY().growX().pad(padding).row() 83 | } 84 | if (table.children.any()) list.add(table) 85 | 86 | if (foldedSide == Side.RIGHT) list.forEach { cont.add(it).fill() } 87 | else list.reversed().forEach { cont.add(it).fill() } 88 | 89 | res.setSize( 90 | res.prefWidth, 91 | targetHeight 92 | ) 93 | } 94 | else { 95 | val cont = res.table().fillY().growX().get().left() 96 | var table = Table() 97 | view.foldCards.forEach { card -> 98 | val elem = buildCardShower(view, card, foldedSide, cardScl, padding) 99 | 100 | if (table.prefWidth + elem.prefWidth > targetWidth) { 101 | list.add(table) 102 | table = Table() 103 | } 104 | table.top().add(elem).fillX().growY().pad(padding) 105 | } 106 | if (table.children.any()) list.add(table) 107 | 108 | if (foldedSide == Side.TOP) list.forEach { cont.add(it).fill().row() } 109 | else list.reversed().forEach { cont.add(it).fill().row() } 110 | 111 | res.setSize( 112 | targetWidth, 113 | res.prefHeight 114 | ) 115 | } 116 | 117 | return res 118 | } 119 | 120 | private fun buildCardShower( 121 | view: DesignerView, 122 | card: Card, 123 | side: Side, 124 | cardScl: Float, 125 | padding: Float 126 | ): Element { 127 | val linker = view.foldLinkers[card] 128 | val dh = card.height*cardScl + padding + linker.height 129 | 130 | return object: Element(){ 131 | val mh = card.height + padding + linker.height 132 | override fun draw() { 133 | validate() 134 | 135 | val dx: Float 136 | val dy: Float 137 | when(side) { 138 | Side.LEFT -> { dx = x + width - card.width*cardScl; dy = y } 139 | Side.RIGHT -> { dx = x; dy = y + height - mh } 140 | Side.TOP -> { dx = x; dy = y } 141 | Side.BOTTOM -> { dx = x; dy = y + height - mh } 142 | } 143 | drawFoldCard( 144 | card, 145 | cardScl, 146 | linker, 147 | dx, dy, 148 | padding 149 | ) 150 | } 151 | 152 | override fun getPrefWidth(): Float { 153 | return card.width*cardScl 154 | } 155 | 156 | override fun getPrefHeight(): Float { 157 | return dh 158 | } 159 | } 160 | } 161 | 162 | private fun drawFoldCard( 163 | card: Card, 164 | cardScl: Float, 165 | foldLinker: FoldLink, 166 | x: Float, 167 | y: Float, 168 | padding: Float, 169 | ) { 170 | val origPar = card.parent 171 | val origLinPar = foldLinker.parent 172 | val origX = card.x 173 | val origY = card.y 174 | val origLinX = foldLinker.x 175 | val origLinY = foldLinker.y 176 | val origTrans = Tmp.m1.set(Draw.trans()) 177 | 178 | foldLinker.parent = null 179 | card.x = 0f 180 | card.y = 0f 181 | foldLinker.x = x + card.width*cardScl/2 - foldLinker.width/2 182 | foldLinker.y = y + card.height*cardScl + padding 183 | 184 | card.singleRend() 185 | card.invalidate() 186 | Draw.trans(Tmp.m2.setToTranslation(x, y).scale(cardScl, cardScl)) 187 | card.isTransform = false 188 | card.draw() 189 | card.isTransform = true 190 | Draw.trans(origTrans) 191 | card.invalidate() 192 | 193 | foldLinker.invalidate() 194 | foldLinker.draw() 195 | foldLinker.invalidate() 196 | 197 | card.parent = origPar 198 | foldLinker.parent = origLinPar 199 | card.x = origX 200 | card.y = origY 201 | foldLinker.x = origLinX 202 | foldLinker.y = origLinY 203 | } 204 | 205 | fun drawCardsContainer( 206 | view: DesignerView, 207 | buff: FrameBuffer, 208 | boundX: Float, 209 | boundY: Float, 210 | scl: Float 211 | ): FrameBuffer { 212 | val bound = view.getBound() 213 | 214 | val width = bound.width + boundX*2 215 | val height = bound.height + boundY*2 216 | 217 | val dx = bound.x - boundX 218 | val dy = bound.y - boundY 219 | 220 | val camera = Camera() 221 | camera.width = width 222 | camera.height = height 223 | camera.position.x = dx + width/2f 224 | camera.position.y = dy + height/2f 225 | camera.update() 226 | 227 | val pan = view.panned 228 | val par = view.container.parent 229 | val x = view.container.x 230 | val y = view.container.y 231 | val sclX = view.zoom.scaleX 232 | val sclY = view.zoom.scaleY 233 | val scW = Core.scene.width 234 | val scH = Core.scene.height 235 | 236 | view.zoom.scaleX = 1f 237 | view.zoom.scaleY = 1f 238 | view.panned = Vec2.ZERO 239 | view.container.parent = null 240 | view.container.x = 0f 241 | view.container.y = 0f 242 | Core.scene.viewport.worldWidth = width 243 | Core.scene.viewport.worldHeight = height 244 | 245 | view.cards.forEach { it.singleRend() } 246 | view.container.draw() 247 | 248 | val imageWidth = (width*scl).toInt() 249 | val imageHeight = (height*scl).toInt() 250 | 251 | buff.resize(imageWidth, imageHeight) 252 | buff.begin(Pal.darkerGray) 253 | Draw.proj(camera) 254 | view.drawToImage() 255 | Draw.flush() 256 | buff.end() 257 | 258 | view.container.parent = par 259 | view.container.x = x 260 | view.container.y = y 261 | view.zoom.scaleX = sclX 262 | view.zoom.scaleY = sclY 263 | view.panned = pan 264 | Core.scene.viewport.worldWidth = scW 265 | Core.scene.viewport.worldHeight = scH 266 | 267 | return buff 268 | } 269 | } -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/designer/StatisticEvent.kt: -------------------------------------------------------------------------------- 1 | package tmi.ui.designer 2 | 3 | import arc.scene.event.SceneEvent 4 | 5 | class StatisticEvent: SceneEvent() { 6 | } -------------------------------------------------------------------------------- /src/main/kotlin/tmi/ui/element/Flex.java: -------------------------------------------------------------------------------- 1 | package tmi.ui.element; 2 | 3 | import arc.func.Cons; 4 | import arc.scene.Element; 5 | import arc.scene.ui.layout.WidgetGroup; 6 | import arc.struct.SnapshotSeq; 7 | 8 | public class Flex extends WidgetGroup { 9 | private boolean sizeInvalidate; 10 | private boolean wrap = true; 11 | 12 | private float prefWidth; 13 | private float prefHeight; 14 | 15 | public Flex(){} 16 | 17 | public Flex(Cons build){ 18 | build.get(this); 19 | } 20 | 21 | public void setWrap(boolean wrap){ 22 | if (wrap != this.wrap) { 23 | this.wrap = wrap; 24 | invalidate(); 25 | } 26 | } 27 | 28 | public boolean getWrap(){ 29 | return wrap; 30 | } 31 | 32 | public void calculateSize(){ 33 | sizeInvalidate = false; 34 | 35 | if (wrap) { 36 | float targetWidth = getWidth(); 37 | 38 | float width = 0; 39 | float height = 0; 40 | 41 | float maxHeight = 0; 42 | 43 | SnapshotSeq seq = getChildren(); 44 | Element[] list = seq.begin(); 45 | for (int i = 0, n = seq.size; i < n; i++) { 46 | Element e = list[i]; 47 | 48 | float w = e.getWidth(); 49 | float h = e.getHeight(); 50 | if (width + w > targetWidth) { 51 | height += maxHeight; 52 | maxHeight = 0; 53 | width = 0; 54 | } 55 | width += w; 56 | maxHeight = Math.max(maxHeight, h); 57 | } 58 | height += maxHeight; 59 | seq.end(); 60 | 61 | prefWidth = targetWidth; 62 | prefHeight = height; 63 | } 64 | else { 65 | float width = 0; 66 | 67 | float maxHeight = 0; 68 | 69 | SnapshotSeq seq = getChildren(); 70 | Element[] list = seq.begin(); 71 | for (int i = 0, n = seq.size; i < n; i++) { 72 | Element e = list[i]; 73 | 74 | float w = e.getWidth(); 75 | float h = e.getHeight(); 76 | 77 | width += w; 78 | maxHeight = Math.max(maxHeight, h); 79 | } 80 | seq.end(); 81 | 82 | prefWidth = width; 83 | prefHeight = maxHeight; 84 | } 85 | } 86 | 87 | @Override 88 | public void invalidate() { 89 | super.invalidate(); 90 | sizeInvalidate = true; 91 | } 92 | 93 | @Override 94 | public float getPrefWidth() { 95 | if (sizeInvalidate) calculateSize(); 96 | 97 | return prefWidth; 98 | } 99 | 100 | @Override 101 | public float getPrefHeight() { 102 | if (sizeInvalidate) calculateSize(); 103 | 104 | return prefHeight; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/util/CombineKeyListener.kt: -------------------------------------------------------------------------------- 1 | package tmi.util 2 | 3 | import arc.Core 4 | import arc.input.KeyCode 5 | import arc.scene.event.InputEvent 6 | import arc.scene.event.InputListener 7 | import arc.struct.Seq 8 | 9 | open class CombineKeyListener( 10 | val keysTree: CombineKeyTree, 11 | val fuzzed: Boolean = false 12 | ): InputListener(){ 13 | private val keysDown = Seq(KeyCode::class.java) 14 | 15 | override fun keyDown(event: InputEvent?, keycode: KeyCode?): Boolean { 16 | if (!keysTree.containsKeyCode(keycode)) return false 17 | keysDown.addUnique(keycode) 18 | 19 | if (keycode!!.isControllerKey()) return true 20 | 21 | if (!fuzzed) { 22 | val map = keysTree.getTargetBindings(Core.input) 23 | if (map.isEmpty()) return false 24 | 25 | val keys = CombinedKeys(*keysDown.toArray()) 26 | map[keys]?.let { keysDown(event, keycode, keys, it) } 27 | } 28 | else { 29 | keysTree.eachTargetBindings(Core.input, true){ k, r -> 30 | if (k.key == keycode) keysDown(event, keycode, k, r) 31 | } 32 | } 33 | 34 | return true 35 | } 36 | 37 | override fun keyUp(event: InputEvent?, keycode: KeyCode?): Boolean { 38 | if (!keysTree.containsKeyCode(keycode)) return false 39 | 40 | if (keycode!!.isControllerKey()){ 41 | return keysDown.remove(keycode) 42 | } 43 | 44 | if (!fuzzed) { 45 | val map = keysTree.getTargetBindings(Core.input) 46 | if (map.isEmpty()) return false 47 | 48 | keysDown.add(keycode) // 使按键抬起时必然监听到最后一个是抬起的按钮 49 | val keys = CombinedKeys(*keysDown.toArray()) 50 | map[keys]?.let { keysUp(event, keycode, keys, it) } 51 | keysDown.remove(keysDown.size - 1) 52 | } 53 | else { 54 | keysTree.eachTargetBindings(Core.input, true){ k, r -> 55 | if (k.key == keycode) keysUp(event, keycode, k, r) 56 | } 57 | } 58 | return keysDown.remove(keycode) 59 | } 60 | 61 | open fun keysDown(event: InputEvent?, keycode: KeyCode?, combinedKeys: CombinedKeys, rec: Rec) {} 62 | open fun keysUp(event: InputEvent?, keycode: KeyCode?, combinedKeys: CombinedKeys, rec: Rec) {} 63 | } -------------------------------------------------------------------------------- /src/main/kotlin/tmi/util/CombineKeyTree.kt: -------------------------------------------------------------------------------- 1 | package tmi.util 2 | 3 | import arc.Input 4 | import arc.func.Cons 5 | import arc.input.KeyCode 6 | 7 | class CombineKeyTree{ 8 | private val tempMap = mutableMapOf() 9 | 10 | private val normalBindings = mutableMapOf() 11 | private val ctrlBindings = mutableMapOf() 12 | private val altBindings = mutableMapOf() 13 | private val shiftBindings = mutableMapOf() 14 | private val altCtrlBindings = mutableMapOf() 15 | private val ctrlShiftBindings = mutableMapOf() 16 | private val altShiftBindings = mutableMapOf() 17 | private val altCtrlShiftBindings = mutableMapOf() 18 | 19 | fun putKeyBinding(binding: CombinedKeys, rec: Rec) { 20 | if (binding.isShift) { 21 | if (binding.isAlt && binding.isCtrl) altCtrlShiftBindings[binding] = rec 22 | else if (binding.isAlt) altShiftBindings[binding] = rec 23 | else if (binding.isCtrl) ctrlShiftBindings[binding] = rec 24 | else shiftBindings[binding] = rec 25 | } 26 | else { 27 | if (binding.isAlt && binding.isCtrl) altCtrlBindings[binding] = rec 28 | else if (binding.isAlt) altBindings[binding] = rec 29 | else if (binding.isCtrl) ctrlBindings[binding] = rec 30 | else normalBindings[binding] = rec 31 | } 32 | } 33 | 34 | fun putKeyBinds(vararg bindings: Pair) = bindings.forEach { putKeyBinding(it.first, it.second) } 35 | 36 | fun clear() { 37 | normalBindings.clear() 38 | altBindings.clear() 39 | ctrlBindings.clear() 40 | shiftBindings.clear() 41 | altShiftBindings.clear() 42 | ctrlShiftBindings.clear() 43 | altCtrlBindings.clear() 44 | altCtrlShiftBindings.clear() 45 | } 46 | 47 | fun forEach(block: (CombinedKeys, Rec) -> Unit) { 48 | normalBindings.forEach(block) 49 | ctrlBindings.forEach(block) 50 | altBindings.forEach(block) 51 | altCtrlBindings.forEach(block) 52 | ctrlShiftBindings.forEach(block) 53 | altShiftBindings.forEach(block) 54 | altCtrlShiftBindings.forEach(block) 55 | } 56 | 57 | fun containsKeyCode(key: KeyCode?): Boolean{ 58 | if (key == null) return false 59 | 60 | if (key.isCtrl()) 61 | return ctrlBindings.isNotEmpty() || ctrlShiftBindings.isNotEmpty() || altCtrlBindings.isNotEmpty() || altCtrlShiftBindings.isNotEmpty() 62 | if (key.isAlt()) 63 | return altBindings.isNotEmpty() || altShiftBindings.isNotEmpty() || altCtrlBindings.isNotEmpty() || altCtrlShiftBindings.isNotEmpty() 64 | if (key.isShift()) 65 | return shiftBindings.isNotEmpty() || ctrlShiftBindings.isNotEmpty() || altShiftBindings.isNotEmpty() || altCtrlShiftBindings.isNotEmpty() 66 | 67 | return normalBindings.any{ it.key.key == key } 68 | || ctrlBindings.any { it.key.key == key } 69 | || altBindings.any { it.key.key == key } 70 | || shiftBindings.any { it.key.key == key } 71 | || altCtrlBindings.any { it.key.key == key } 72 | || ctrlShiftBindings.any { it.key.key == key } 73 | || altShiftBindings.any { it.key.key == key } 74 | || altCtrlShiftBindings.any { it.key.key == key } 75 | } 76 | 77 | fun getTargetBindings(input: Input, fuzzyMatch: Boolean = false) = getTargetBindings( 78 | input.ctrl(), 79 | input.alt(), 80 | input.shift(), 81 | fuzzyMatch 82 | ) 83 | 84 | fun getTargetBindings(input: CombinedKeys, fuzzyMatch: Boolean = false) = getTargetBindings( 85 | input.isCtrl, 86 | input.isAlt, 87 | input.isShift, 88 | fuzzyMatch 89 | ) 90 | 91 | fun getTargetBindings( 92 | ctrlDown: Boolean, 93 | altDown: Boolean, 94 | shiftDown: Boolean, 95 | fuzzyMatch: Boolean = false 96 | ): Map{ 97 | tempMap.clear() 98 | if (fuzzyMatch){ 99 | tempMap.putAll(normalBindings) 100 | if (ctrlDown){ 101 | tempMap.putAll(ctrlBindings) 102 | if (altDown){ 103 | tempMap.putAll(altCtrlBindings) 104 | if (shiftDown) tempMap.putAll(altCtrlShiftBindings) 105 | } 106 | else if (shiftDown) tempMap.putAll(ctrlShiftBindings) 107 | } 108 | else { 109 | if (altDown){ 110 | tempMap.putAll(altBindings) 111 | if (shiftDown) tempMap.putAll(altShiftBindings) 112 | } 113 | else if (shiftDown) tempMap.putAll(shiftBindings) 114 | } 115 | return tempMap 116 | } 117 | else { 118 | return if (shiftDown) { 119 | if (altDown) if (ctrlDown) altCtrlShiftBindings else altShiftBindings 120 | else if (ctrlDown) ctrlShiftBindings else shiftBindings 121 | } 122 | else { 123 | if (altDown) if (ctrlDown) altCtrlBindings else altBindings 124 | else if (ctrlDown) ctrlBindings else normalBindings 125 | } 126 | } 127 | } 128 | 129 | inline fun eachTargetBindings(input: Input, fuzzyMatch: Boolean = false, cons: (CombinedKeys, Rec) -> Unit){ 130 | for ((k, r) in getTargetBindings(input, fuzzyMatch)) cons(k, r) 131 | } 132 | 133 | fun eachDown(input: Input, fuzzyMatch: Boolean = false, cons: Cons) { 134 | eachTargetBindings(input, fuzzyMatch){ k, r -> if (input.keyDown(k.key)) cons.get(r) } 135 | } 136 | 137 | fun eachRelease(input: Input, fuzzyMatch: Boolean = false, cons: Cons) { 138 | eachTargetBindings(input, fuzzyMatch){ k, r -> if (input.keyRelease(k.key)) cons.get(r) } 139 | } 140 | 141 | fun eachTap(input: Input, fuzzyMatch: Boolean = false, cons: Cons) { 142 | eachTargetBindings(input, fuzzyMatch){ k, r -> if (input.keyTap(k.key)) cons.get(r) } 143 | } 144 | 145 | fun checkDown(input: Input): Rec? { 146 | eachTargetBindings(input){ k, r -> if (input.keyDown(k.key)) return r } 147 | return null 148 | } 149 | 150 | fun checkRelease(input: Input): Rec? { 151 | eachTargetBindings(input){ k, r -> if (input.keyRelease(k.key)) return r } 152 | return null 153 | } 154 | 155 | fun checkTap(input: Input): Rec? { 156 | eachTargetBindings(input){ k, r -> if (input.keyTap(k.key)) return r } 157 | return null 158 | } 159 | 160 | override fun toString(): String { 161 | val stringBuilder = StringBuilder() 162 | forEach { keys, rec -> 163 | stringBuilder.append("$keys -> $rec,\n") 164 | } 165 | return stringBuilder.toString() 166 | } 167 | } -------------------------------------------------------------------------------- /src/main/kotlin/tmi/util/CombinedKeys.kt: -------------------------------------------------------------------------------- 1 | package tmi.util 2 | 3 | import arc.Input 4 | import arc.input.KeyCode 5 | import java.io.Serializable 6 | 7 | /**一个记录特定格式的标准组合键的标记对象 8 | * 9 | * 一个组合键由主键和控制键构成,其中控制键为`Ctrl`,*/ 10 | class CombinedKeys(vararg keys: KeyCode): Serializable { 11 | val isCtrl: Boolean 12 | val isAlt: Boolean 13 | val isShift: Boolean 14 | val key: KeyCode 15 | 16 | init { 17 | val ctrl = keys.filter { it.isCtrl() } 18 | val alt = keys.filter { it.isAlt() } 19 | val shift = keys.filter { it.isShift() } 20 | val normal = keys.filter { !ctrl.contains(it) && !alt.contains(it) && !shift.contains(it) } 21 | isCtrl = ctrl.isNotEmpty() 22 | isAlt = alt.isNotEmpty() 23 | isShift = shift.isNotEmpty() 24 | key = normal.lastOrNull()?:throw IllegalArgumentException("No key specified") 25 | } 26 | 27 | fun isDown(input: Input): Boolean{ 28 | if ((isCtrl && !input.ctrl()) || (!isCtrl && input.alt())) return false 29 | if ((isAlt && !input.alt()) || (!isAlt && input.alt())) return false 30 | if ((isShift && !input.shift()) || (!isShift && input.shift())) return false 31 | return input.keyDown(key) 32 | } 33 | 34 | fun isReleased(input: Input): Boolean{ 35 | if ((isCtrl && !input.ctrl()) || (!isCtrl && input.alt())) return false 36 | if ((isAlt && !input.alt()) || (!isAlt && input.alt())) return false 37 | if ((isShift && !input.shift()) || (!isShift && input.shift())) return false 38 | return input.keyRelease(key) 39 | } 40 | 41 | fun isTap(input: Input): Boolean{ 42 | if ((isCtrl && !input.ctrl()) || (!isCtrl && input.alt())) return false 43 | if ((isAlt && !input.alt()) || (!isAlt && input.alt())) return false 44 | if ((isShift && !input.shift()) || (!isShift && input.shift())) return false 45 | return input.keyTap(key) 46 | } 47 | 48 | override fun toString(): String { 49 | val builder = StringBuilder() 50 | 51 | if (isCtrl) builder.append("Ctrl").append(" + ") 52 | if (isAlt) builder.append("Alt").append(" + ") 53 | if (isShift) builder.append("Shift").append(" + ") 54 | 55 | builder.append(key.value) 56 | 57 | return builder.toString() 58 | } 59 | 60 | override fun hashCode(): Int { 61 | var res = key.hashCode() 62 | res = res*31 + isShift.hashCode() 63 | res = res*31 + isAlt.hashCode() 64 | res = res*31 + isCtrl.hashCode() 65 | return res 66 | } 67 | 68 | override fun equals(other: Any?): Boolean { 69 | return other is CombinedKeys 70 | && other.key == key 71 | && other.isCtrl == isCtrl 72 | && other.isAlt == isAlt 73 | && other.isShift == isShift 74 | } 75 | 76 | companion object { 77 | @JvmStatic 78 | fun toString(res: Iterable): String { 79 | val builder = StringBuilder() 80 | 81 | if (res.contains(KeyCode.controlLeft) || res.contains(KeyCode.controlRight)) builder.append("Ctrl").append(" + ") 82 | if (res.contains(KeyCode.altLeft) || res.contains(KeyCode.altRight)) builder.append("Alt").append(" + ") 83 | if (res.contains(KeyCode.shiftLeft) || res.contains(KeyCode.shiftRight)) builder.append("Shift").append(" + ") 84 | 85 | builder.append(res.lastOrNull { !it.isControllerKey() }?.value?:"") 86 | 87 | return builder.toString() 88 | } 89 | } 90 | } 91 | 92 | fun KeyCode.isControllerKey() 93 | = this == KeyCode.controlLeft || this == KeyCode.controlRight 94 | || this == KeyCode.altLeft || this == KeyCode.altRight 95 | || this == KeyCode.shiftLeft || this == KeyCode.shiftRight 96 | 97 | fun KeyCode.isCtrl() = this == KeyCode.controlLeft || this == KeyCode.controlRight 98 | fun KeyCode.isAlt() = this == KeyCode.altLeft || this == KeyCode.altRight 99 | fun KeyCode.isShift() = this == KeyCode.shiftLeft || this == KeyCode.shiftRight 100 | 101 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/util/Consts.kt: -------------------------------------------------------------------------------- 1 | package tmi.util 2 | 3 | import arc.Core 4 | import arc.func.Prov 5 | import arc.graphics.Color 6 | import arc.graphics.g2d.Lines 7 | import arc.scene.style.BaseDrawable 8 | import arc.scene.style.Drawable 9 | import arc.scene.style.TextureRegionDrawable 10 | import arc.scene.ui.Dialog.DialogStyle 11 | import arc.scene.ui.layout.Scl 12 | import arc.struct.Seq 13 | import arc.util.Tmp 14 | import mindustry.content.Blocks 15 | import mindustry.game.Team 16 | import mindustry.gen.Building 17 | import mindustry.gen.Icon 18 | import mindustry.gen.Tex 19 | import mindustry.graphics.Pal 20 | import mindustry.ui.Fonts 21 | import mindustry.world.Block 22 | import mindustry.world.Tile 23 | import mindustry.world.blocks.environment.Floor 24 | import java.lang.reflect.Field 25 | 26 | object Consts { 27 | private val emp: Seq<*> = Seq() 28 | 29 | val buildTimeAlter: Field = try { 30 | Block::class.java.getField("buildTime") 31 | } catch (e: NoSuchFieldException) { 32 | Block::class.java.getField("buildCost") 33 | } 34 | 35 | val foldCardIcons: List by lazy { 36 | Icon.icons.map { it.key to it.value }.sortedBy { it.first }.filter { !it.first.contains("Small") }.map { it.second } 37 | } 38 | 39 | val grayUI: Drawable by lazy { (Tex.whiteui as TextureRegionDrawable).tint(Tmp.c1.set(Pal.darkerGray)) } 40 | val darkGrayUI: Drawable by lazy { (Tex.whiteui as TextureRegionDrawable).tint(Tmp.c1.set(Pal.darkestGray)) } 41 | val midGrayUI: Drawable by lazy { (Tex.whiteui as TextureRegionDrawable).tint(Tmp.c1.set(Pal.darkerGray).lerp(Pal.darkestGray, 0.7f)) } 42 | val grayUIAlpha: Drawable by lazy { (Tex.whiteui as TextureRegionDrawable).tint(Tmp.c1.set(Pal.darkerGray).a(0.7f)) } 43 | val darkGrayUIAlpha: Drawable by lazy { (Tex.whiteui as TextureRegionDrawable).tint(Tmp.c1.set(Pal.darkestGray).a(0.7f)) } 44 | val padGrayUI: Drawable by lazy { 45 | val res = (Tex.whiteui as TextureRegionDrawable).tint(Tmp.c1.set(Pal.darkerGray)) 46 | res.leftWidth = 8f 47 | res.rightWidth = 8f 48 | res.topHeight = 8f 49 | res.bottomHeight = 8f 50 | return@lazy res 51 | } 52 | val padDarkGrayUI: Drawable by lazy { 53 | val res = (Tex.whiteui as TextureRegionDrawable).tint(Tmp.c1.set(Pal.darkestGray)) 54 | res.leftWidth = 8f 55 | res.rightWidth = 8f 56 | res.topHeight = 8f 57 | res.bottomHeight = 8f 58 | return@lazy res 59 | } 60 | val padGrayUIAlpha: Drawable by lazy { 61 | val res = (Tex.whiteui as TextureRegionDrawable).tint(Tmp.c1.set(Pal.darkerGray).a(0.7f)) 62 | res.leftWidth = 8f 63 | res.rightWidth = 8f 64 | res.topHeight = 8f 65 | res.bottomHeight = 8f 66 | return@lazy res 67 | } 68 | val padDarkGrayUIAlpha: Drawable by lazy { 69 | val res = (Tex.whiteui as TextureRegionDrawable).tint(Tmp.c1.set(Pal.darkestGray).a(0.7f)) 70 | res.leftWidth = 8f 71 | res.rightWidth = 8f 72 | res.topHeight = 8f 73 | res.bottomHeight = 8f 74 | return@lazy res 75 | } 76 | val leftLine by lazy { object : BaseDrawable(){ 77 | init { 78 | leftWidth = 4f 79 | rightWidth = 4f 80 | topHeight = 4f 81 | bottomHeight = 4f 82 | } 83 | 84 | override fun draw(x: Float, y: Float, width: Float, height: Float) { 85 | val lw = Scl.scl(3f) 86 | Lines.stroke(lw, Color.gray) 87 | Lines.line(x + lw/2f, y + lw, x + lw/2f, y + height - lw) 88 | } 89 | } } 90 | val a_z: Drawable by lazy { Core.atlas.getDrawable("tmi-a_z") } 91 | val tmi: Drawable by lazy { Core.atlas.getDrawable("tmi-tmi") } 92 | val panner: Drawable by lazy { Core.atlas.getDrawable("tmi-panner") } 93 | val balance: Drawable by lazy { Core.atlas.getDrawable("tmi-balance") } 94 | val inbalance: Drawable by lazy { Core.atlas.getDrawable("tmi-inbalance") } 95 | val time: Drawable by lazy { Core.atlas.getDrawable("tmi-time") } 96 | val clip: Drawable by lazy { Core.atlas.getDrawable("tmi-clip") } 97 | 98 | val side_bottom: Drawable by lazy { Core.atlas.getDrawable("tmi-side_bottom") } 99 | val side_top: Drawable by lazy { Core.atlas.getDrawable("tmi-side_top") } 100 | val side_left: Drawable by lazy { Core.atlas.getDrawable("tmi-side_left") } 101 | val side_right: Drawable by lazy { Core.atlas.getDrawable("tmi-side_right") } 102 | 103 | val transparent: Drawable by lazy { (Tex.whiteui as TextureRegionDrawable).tint(Color.clear) } 104 | 105 | val transparentBack: DialogStyle by lazy { object : DialogStyle() { 106 | init { 107 | stageBackground = transparent 108 | titleFont = Fonts.outline 109 | background = transparent 110 | titleFontColor = Pal.accent 111 | } 112 | }} 113 | 114 | val markerTile: Tile = object : Tile(0, 0) { 115 | override fun setFloor(type: Floor) { 116 | this.floor = type 117 | this.overlay = Blocks.air as Floor 118 | } 119 | 120 | override fun setOverlay(block: Block) { 121 | this.overlay = block as Floor 122 | } 123 | 124 | override fun setBlock(type: Block, team: Team, rotation: Int, entityprov: Prov) { 125 | this.block = type 126 | this.build = entityprov.get() 127 | build.team = team 128 | build.rotation = rotation 129 | } 130 | } 131 | 132 | @Suppress("UNCHECKED_CAST") 133 | fun empSeq(): Seq { 134 | return emp as Seq 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/util/Geom.kt: -------------------------------------------------------------------------------- 1 | package tmi.util 2 | 3 | import arc.math.Angles 4 | import arc.math.geom.Vec2 5 | 6 | object Geom { 7 | fun angleToD4Integer(x: Float, y: Float, width: Float, height: Float) = angleToD4Integer( 8 | Angles.angle(x, y), 9 | Angles.angle(width, height) 10 | ) 11 | 12 | fun angleToD4Integer(angle: Float, width: Float, height: Float) = angleToD4Integer( 13 | angle, 14 | Angles.angle(width, height) 15 | ) 16 | 17 | fun angleToD4Integer(angle: Float, check: Float = 45f): Int { 18 | val a = angle%360 19 | val c = check%360 20 | 21 | return when { 22 | a > c && a < 180 - c -> 1 23 | a > 180 - c && a < 180 + c -> 2 24 | a > 180 + c && a < 360 - c -> 3 25 | else -> 0 26 | } 27 | } 28 | 29 | fun getRectNearest(res: Vec2, x: Float, y: Float, botLeft: Vec2, topRight: Vec2): Vec2 { 30 | val centX = (botLeft.x + topRight.x) / 2 31 | val centY = (botLeft.y + topRight.y) / 2 32 | 33 | res.x = if (x < centX) botLeft.x else topRight.x 34 | res.y = if (y < centY) botLeft.y else topRight.y 35 | 36 | return res 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/tmi/util/KeyBinds.kt: -------------------------------------------------------------------------------- 1 | package tmi.util 2 | 3 | import arc.input.KeyBind 4 | import arc.input.KeyCode 5 | 6 | class KeyBinds { 7 | val hotKey = KeyBind.add("tmi_hot_key", KeyCode.controlLeft, "tmi") 8 | 9 | fun load() { 10 | hotKey.load() 11 | } 12 | 13 | fun reset(name: String?) { 14 | when (name) { 15 | "hot_key" -> hotKey.resetToDefault() 16 | } 17 | save() 18 | } 19 | 20 | fun save() { 21 | hotKey.save() 22 | } 23 | 24 | fun resetAll() { 25 | hotKey.resetToDefault() 26 | save() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/util/Shapes.kt: -------------------------------------------------------------------------------- 1 | package tmi.util 2 | 3 | import arc.Core 4 | import arc.graphics.Color 5 | import arc.graphics.g2d.Draw 6 | import arc.graphics.g2d.Fill 7 | import arc.graphics.g2d.Lines 8 | import arc.graphics.g2d.TextureRegion 9 | import arc.math.Mathf.* 10 | import kotlin.math.abs 11 | 12 | object Shapes { 13 | /**在指定位置绘制一个扇形 14 | * 15 | * @param x 扇形的圆心x坐标 16 | * @param y 扇形的圆心y坐标 17 | * @param radius 扇形所属圆形的半径 18 | * @param angle 扇形的内角 19 | * @param rotate 扇形的旋转角,即扇形的右侧边相对x轴的角度 20 | * @param segments 扇形精细度*/ 21 | @JvmStatic 22 | @JvmOverloads 23 | fun fan( 24 | x: Float, y: Float, radius: Float, angle: Float, 25 | rotate: Float = 0f, 26 | segments: Int = abs(Lines.circleVertices(radius)*(angle/360f)).toInt(), 27 | ) { 28 | val segmentAngle = angle / segments 29 | 30 | for (i in 0 until segments) { 31 | val currentAngle = rotate + segmentAngle * i 32 | 33 | val startX = x + radius * cosDeg(currentAngle) 34 | val startY = y + radius * sinDeg(currentAngle) 35 | val endX = x + radius * cosDeg(currentAngle + segmentAngle) 36 | val endY = y + radius * sinDeg(currentAngle + segmentAngle) 37 | 38 | Fill.tri(x, y, startX, startY, endX, endY) 39 | } 40 | } 41 | 42 | fun line(x: Float, y: Float, c: Color, x2: Float, y2: Float, c2: Color) { 43 | line(x, y, c, x2, y2, c2, true) 44 | } 45 | 46 | fun line(x: Float, y: Float, c: Color, x2: Float, y2: Float, c2: Color, cap: Boolean) { 47 | val hstroke = Lines.getStroke()/2f 48 | val len = len(x2 - x, y2 - y) 49 | val diffx = (x2 - x)/len*hstroke 50 | val diffy = (y2 - y)/len*hstroke 51 | 52 | if (cap) { 53 | Fill.quad( 54 | x - diffx - diffy, 55 | y - diffy + diffx, 56 | c.toFloatBits(), 57 | 58 | x - diffx + diffy, 59 | y - diffy - diffx, 60 | c.toFloatBits(), 61 | 62 | x2 + diffx + diffy, 63 | y2 + diffy - diffx, 64 | c2.toFloatBits(), 65 | 66 | x2 + diffx - diffy, 67 | y2 + diffy + diffx, 68 | c2.toFloatBits() 69 | ) 70 | } 71 | else { 72 | Fill.quad( 73 | x - diffy, 74 | y + diffx, 75 | c.toFloatBits(), 76 | 77 | x + diffy, 78 | y - diffx, 79 | c.toFloatBits(), 80 | 81 | x2 + diffy, 82 | y2 - diffx, 83 | c2.toFloatBits(), 84 | 85 | x2 - diffy, 86 | y2 + diffx, 87 | c2.toFloatBits() 88 | ) 89 | } 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /src/main/kotlin/tmi/util/Temps.kt: -------------------------------------------------------------------------------- 1 | package tmi.util 2 | 3 | import arc.graphics.Color 4 | import arc.math.geom.Rect 5 | import arc.math.geom.Vec2 6 | import arc.scene.style.TextureRegionDrawable 7 | 8 | val color1 = Color() 9 | val color2 = Color() 10 | val color3 = Color() 11 | 12 | val rect1 = Rect() 13 | val rect2 = Rect() 14 | val rect3 = Rect() 15 | 16 | val vec1 = Vec2() 17 | val vec2 = Vec2() 18 | val vec3 = Vec2() 19 | val vec4 = Vec2() 20 | 21 | val drawable1 = TextureRegionDrawable() 22 | val drawable2 = TextureRegionDrawable() 23 | val drawable3 = TextureRegionDrawable() 24 | -------------------------------------------------------------------------------- /src/main/kotlin/tmi/util/Utils.kt: -------------------------------------------------------------------------------- 1 | package tmi.util 2 | 3 | object Utils { 4 | private var globalUnit: Unit? = null 5 | 6 | fun unitTimed(v: Float, uncheckGlobal: Boolean = false): Pair { 7 | if (uncheckGlobal || globalUnit == null) { 8 | var value = v*60 9 | var unit = Unit.SEC 10 | 11 | if (value < 1) { value *= 60; unit = Unit.MIN } 12 | if (value < 1) { value *= 60; unit = Unit.HOUR } 13 | if (value < 1) { value *= 24; unit = Unit.DAY } // 这可能吗? Is this possible? 14 | 15 | return value to unit 16 | } 17 | else globalUnit!!.let { return v*it.multi to it } 18 | } 19 | 20 | fun mandatoryGlobalUnit(unit: Unit) { 21 | globalUnit = unit 22 | } 23 | 24 | fun releaseGlobalUnit() { 25 | globalUnit = null 26 | } 27 | 28 | enum class Unit( 29 | private val strify: String, 30 | val multi: Float, 31 | ) { 32 | TICK("[white]/tick", 1f), 33 | SEC("[gray]/sec", 60f), 34 | MIN("[lightgray]/min", 60f * 60f), 35 | HOUR("[red]/hour", 60f * 60f * 60f), 36 | DAY("[crimson]/day", 60f * 60f * 60f * 24f); 37 | 38 | override fun toString(): String = strify 39 | fun next(): Unit = when(this) { 40 | TICK -> SEC 41 | SEC -> MIN 42 | MIN -> HOUR 43 | HOUR -> DAY 44 | DAY -> TICK 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/tmi/util/WrapAppListener.kt: -------------------------------------------------------------------------------- 1 | package tmi.util 2 | 3 | import arc.Application 4 | import arc.ApplicationListener 5 | import arc.Core 6 | import arc.files.Fi 7 | import arc.func.Cons 8 | import tmi.invoke 9 | 10 | open class WrapAppListener( 11 | protected val origin: ApplicationListener, 12 | private var proxy: Boolean = false, 13 | private val reset: Cons, 14 | ): ApplicationListener { 15 | fun reset(){ 16 | reset.get(origin) 17 | } 18 | 19 | override fun init() { 20 | if (proxy) origin.init() 21 | } 22 | 23 | override fun resize(width: Int, height: Int) { 24 | if (proxy) origin.resize(width, height) 25 | } 26 | 27 | override fun update() { 28 | if (proxy) origin.update() 29 | } 30 | 31 | override fun pause() { 32 | if (proxy) origin.pause() 33 | } 34 | 35 | override fun resume() { 36 | if (proxy) origin.resume() 37 | } 38 | 39 | override fun dispose() { 40 | if (proxy) origin.dispose() 41 | } 42 | 43 | override fun exit() { 44 | if (proxy) origin.exit() 45 | } 46 | 47 | override fun fileDropped(file: Fi?) { 48 | if (proxy) origin.fileDropped(file) 49 | } 50 | } -------------------------------------------------------------------------------- /src/test/kotlin/Test.kt: -------------------------------------------------------------------------------- 1 | class Test { 2 | private var n: Int? = 10 3 | 4 | fun foo1(a: Int){ 5 | println(n?:10) 6 | println(a + n!!) 7 | } 8 | } --------------------------------------------------------------------------------