├── .gitignore ├── src ├── main │ ├── ui │ │ ├── component │ │ │ ├── settings │ │ │ │ ├── SettingsPage.scss │ │ │ │ └── SettingsPage.tsx │ │ │ ├── table │ │ │ │ └── TableComponent.scss │ │ │ ├── navbar │ │ │ │ ├── NavBar.scss │ │ │ │ └── NavBar.tsx │ │ │ ├── fileUploader │ │ │ │ ├── FileUploader.scss │ │ │ │ └── FileUploader.tsx │ │ │ ├── config │ │ │ │ ├── BaseInventOmatic.scss │ │ │ │ ├── HUDEditor.scss │ │ │ │ ├── configs.ts │ │ │ │ ├── schema-form │ │ │ │ │ └── Components.tsx │ │ │ │ ├── schema.ts │ │ │ │ ├── InventOmatic.tsx │ │ │ │ ├── HUDEditor.tsx │ │ │ │ └── pipboySchema.ts │ │ │ └── dialog │ │ │ │ ├── InfoDialog.tsx │ │ │ │ └── ItemStatsDialog.tsx │ │ ├── index.scss │ │ ├── service │ │ │ ├── Routes.tsx │ │ │ ├── localStorage.service.ts │ │ │ ├── base.service.ts │ │ │ ├── item.service.ts │ │ │ ├── game.api.service.ts │ │ │ ├── tableSettings.service.ts │ │ │ ├── rogue.service.ts │ │ │ ├── domain.ts │ │ │ ├── utils.ts │ │ │ ├── table.columns.tsx │ │ │ └── filter.service.ts │ │ └── index.tsx │ ├── resources │ │ ├── static │ │ │ ├── favicon.ico │ │ │ ├── index.html │ │ │ └── error.html │ │ ├── configs │ │ │ ├── armor.grade.config.json │ │ │ └── special.cases.config.json │ │ └── application.properties │ └── java │ │ └── com │ │ └── manson │ │ └── fo76 │ │ ├── service │ │ ├── UrlConfig.java │ │ ├── AppInfo.java │ │ ├── JsonParser.java │ │ ├── BaseRestClient.java │ │ ├── ItemService.java │ │ ├── HudEditorConfigService.java │ │ ├── PriceRequestBuilder.java │ │ ├── GameConfigHolderService.java │ │ ├── Fed76Service.java │ │ ├── PriceCheckAdapter.java │ │ └── GameConfigService.java │ │ ├── repository │ │ ├── ReportedItemRepository.java │ │ └── PriceCheckRepository.java │ │ ├── domain │ │ ├── ItemContext.java │ │ ├── ReportedItem.java │ │ ├── config │ │ │ ├── GitConfig.java │ │ │ ├── Fed76Config.java │ │ │ └── MongoDbConfig.java │ │ └── fed76 │ │ │ ├── PriceCheckCacheItem.java │ │ │ └── PriceEnhanceRequest.java │ │ ├── Main.java │ │ ├── web │ │ ├── MainController.java │ │ ├── GenericErrorController.java │ │ └── api │ │ │ ├── FilterFlagResponse.java │ │ │ ├── ItemController.java │ │ │ └── GameApi.java │ │ ├── config │ │ ├── Swagger2UiConfiguration.java │ │ └── AppConfig.java │ │ └── helper │ │ ├── JFlatCustom.java │ │ └── Utils.java └── test │ ├── java │ ├── com │ │ ├── fo76 │ │ │ ├── jdump │ │ │ │ ├── domain │ │ │ │ │ ├── Mnam.java │ │ │ │ │ ├── Desc.java │ │ │ │ │ ├── Auuv.java │ │ │ │ │ ├── Model.java │ │ │ │ │ ├── Enlt.java │ │ │ │ │ ├── JDumpDescriptor.java │ │ │ │ │ ├── FormId.java │ │ │ │ │ ├── JDumpEntry.java │ │ │ │ │ └── RecordHeader.java │ │ │ │ ├── SqlLiteEntity.java │ │ │ │ ├── Queries.java │ │ │ │ └── ConfigUpdaterTest.java │ │ │ ├── NukaCryptService.kt │ │ │ ├── XTranslatorParser.kt │ │ │ └── ReportedItemsTests.java │ │ └── DebugTests.java │ ├── Fo76DumpTest.java │ └── PopulateLegModsConfig.kt │ └── resources │ └── hibernate.cfg.xml ├── Procfile ├── xTranslatorRequests ├── tsconfig.json ├── README.md ├── .github └── workflows │ └── maven.yml ├── webpack.config.ts ├── package.json └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | *.iml 4 | node_modules/ 5 | built/ 6 | test_resources/ -------------------------------------------------------------------------------- /src/main/ui/component/settings/SettingsPage.scss: -------------------------------------------------------------------------------- 1 | .divider { 2 | margin-top: 1rem; 3 | margin-bottom: 1rem; 4 | } -------------------------------------------------------------------------------- /src/main/ui/index.scss: -------------------------------------------------------------------------------- 1 | .MuiTooltip-popper > .MuiTooltip-tooltip.MuiTooltip-tooltipPlacementBottom { 2 | font-size: 15px; 3 | } -------------------------------------------------------------------------------- /src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdaskaliesku/fo76tradeServer/HEAD/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /src/main/ui/service/Routes.tsx: -------------------------------------------------------------------------------- 1 | export const routes = { 2 | HOME: "/", 3 | SETTINGS: "/settings", 4 | InventOmatic: "/InventOmatic", 5 | HUDEditor: "/HUDEditor", 6 | } -------------------------------------------------------------------------------- /src/test/java/com/fo76/jdump/domain/Mnam.java: -------------------------------------------------------------------------------- 1 | package com.fo76.jdump.domain; 2 | 3 | import com.manson.domain.AbstractObject; 4 | 5 | public class Mnam extends AbstractObject { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/com/fo76/jdump/domain/Desc.java: -------------------------------------------------------------------------------- 1 | package com.fo76.jdump.domain; 2 | 3 | 4 | import com.manson.domain.AbstractObject; 5 | 6 | public class Desc extends AbstractObject { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/service/UrlConfig.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.service; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class UrlConfig { 7 | 8 | private String name; 9 | private String url; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/ui/component/table/TableComponent.scss: -------------------------------------------------------------------------------- 1 | .variable-row > .copy-to-clipboard-container { 2 | visibility: hidden; 3 | } 4 | 5 | * > #stfDialogPlace > * { 6 | font-size: 12px; 7 | > * { 8 | text-transform: lowercase; 9 | } 10 | } -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java $JAVA_OPTS -Dserver.port=$PORT -Dmongo.user=$mongo_user -Dmongo.password=$mongo_password -Dmongo.db=$mongo_db -Dmongo.url=$mongo_url -Dgit.build.time=$HEROKU_RELEASE_CREATED_AT -Dgit.commit.id=$HEROKU_SLUG_COMMIT -jar target/fo76tradeServer-1.3-SNAPSHOT.jar -------------------------------------------------------------------------------- /src/main/resources/configs/armor.grade.config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "armorGrade": "Light", 4 | "armorId": "00182E78" 5 | }, 6 | { 7 | "armorGrade": "Sturdy", 8 | "armorId": "00182E79" 9 | }, 10 | { 11 | "armorGrade": "Heavy", 12 | "armorId": "00182E7A" 13 | } 14 | ] -------------------------------------------------------------------------------- /src/test/java/com/fo76/jdump/domain/Auuv.java: -------------------------------------------------------------------------------- 1 | package com.fo76.jdump.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class Auuv { 6 | @JsonProperty("Unknown") 7 | private String unknown; 8 | 9 | @JsonProperty("Padding?") 10 | private String padding; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/repository/ReportedItemRepository.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.repository; 2 | 3 | import com.manson.fo76.domain.ReportedItem; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | 6 | public interface ReportedItemRepository extends MongoRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/domain/ItemContext.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.domain; 2 | 3 | import lombok.Data; 4 | import lombok.ToString; 5 | 6 | @Data 7 | @ToString 8 | public class ItemContext { 9 | 10 | private boolean priceCheck = false; 11 | private boolean shortenResponse = false; 12 | private boolean fed76Enhance = false; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/domain/ReportedItem.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.domain; 2 | 3 | import com.manson.domain.itemextractor.ItemResponse; 4 | import java.util.Date; 5 | import lombok.Data; 6 | 7 | @Data 8 | public class ReportedItem { 9 | 10 | private ItemResponse item; 11 | private String reason; 12 | private Date date = new Date(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fallout76 trading hub 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/Main.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Main { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Main.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/configs/special.cases.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "Самодельный" : "00113854", 3 | "Напалмовый" : "000E5881", 4 | "Heart Wrencher" : "000D83BF", 5 | "Napalmer" : "000E5881", 6 | "Автоматический гранатомет" : "00182634", 7 | "MIRV" : "000BD56F", 8 | "Inkwell's Quill" : "0008C14D", 9 | "Handmade" : "00113854", 10 | "M79" : "0008F0EF", 11 | "Auto Grenade Launcher" : "00182634" 12 | } -------------------------------------------------------------------------------- /src/main/resources/static/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fallout76 trading hub 7 | 8 | 9 | 10 |

Oops, something wrong has happened

11 | 12 | Go to main page 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/test/java/com/fo76/jdump/domain/Model.java: -------------------------------------------------------------------------------- 1 | package com.fo76.jdump.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.manson.domain.AbstractObject; 5 | 6 | public class Model extends AbstractObject { 7 | 8 | @JsonProperty("ENLS") 9 | private String enls; 10 | 11 | @JsonProperty("ENLT") 12 | private Enlt enlt; 13 | 14 | @JsonProperty("AUUV") 15 | private Auuv auuv; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/ui/component/navbar/NavBar.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top: 0; 3 | margin-left: 0; 4 | margin-right: 0; 5 | background-color: #20262e; 6 | } 7 | 8 | .p-menubar { 9 | border-radius: 0; 10 | font-size: 14px; 11 | 12 | * { 13 | font-size: 14px; 14 | } 15 | 16 | & .p-submenu-list { 17 | width: max-content; 18 | min-width: 12.5rem; 19 | } 20 | } 21 | 22 | .button-link { 23 | text-decoration: none; 24 | } -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/domain/config/GitConfig.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.domain.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Data 8 | @Component 9 | public class GitConfig { 10 | 11 | @Value("${git.build.time:}") 12 | private String buildTimestamp; 13 | 14 | @Value("${git.commit.id:}") 15 | private String gitCommitId; 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/com/fo76/jdump/domain/Enlt.java: -------------------------------------------------------------------------------- 1 | package com.fo76.jdump.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.manson.domain.AbstractObject; 5 | 6 | public class Enlt extends AbstractObject { 7 | @JsonProperty("Red") 8 | private String red; 9 | @JsonProperty("Green") 10 | private String green; 11 | @JsonProperty("Blue") 12 | private String blue; 13 | @JsonProperty("Alpha") 14 | private String alpha; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/com/fo76/jdump/domain/JDumpDescriptor.java: -------------------------------------------------------------------------------- 1 | package com.fo76.jdump.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import java.util.Map; 5 | 6 | public class JDumpDescriptor { 7 | 8 | @JsonProperty("OMOD") 9 | private Map omod; 10 | 11 | public Map getOmod() { 12 | return omod; 13 | } 14 | 15 | public void setOmod(Map omod) { 16 | this.omod = omod; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /xTranslatorRequests: -------------------------------------------------------------------------------- 1 | Legendary mods: 2 | mod_Legendary_ 3 | 4 | Ammo: 5 | AMMO:FULL 6 | 7 | Stats/Parameters: 8 | AVIF:FULL 9 | 10 | Modifications: 11 | mod 12 | OMOD:FULL 13 | 14 | dn_Common 15 | INNR:WNAM 16 | 17 | Armor: 18 | ^Armor_.*$ 19 | ARMO 20 | 21 | Weapons: 22 | ^(?!Default|Epic|Random|Standar|Babylon|Simple).*$ 23 | ^(?!test|zzz|cr|Test|TEST|debug|POST|Temp|DEPRECATED|ZZZ|W05|CUT|cut|MTNL|Protest|ATX|DLC05W|atx|COMP|Survival|Workshop|MT|QA|Pharma|V96|Deleted|DELETED|P01).*$ -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "allowSyntheticDefaultImports": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": false, 14 | "jsx": "react" 15 | }, 16 | "include": ["src"] 17 | } -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/repository/PriceCheckRepository.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.repository; 2 | 3 | import com.manson.fo76.domain.fed76.PriceCheckCacheItem; 4 | import java.util.List; 5 | import org.springframework.data.mongodb.repository.MongoRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public interface PriceCheckRepository extends MongoRepository { 10 | 11 | List findByRequestId(String requestId); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/domain/fed76/PriceCheckCacheItem.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.domain.fed76; 2 | 3 | import com.manson.domain.fed76.response.BasePriceCheckResponse; 4 | import lombok.Data; 5 | import org.springframework.data.annotation.Id; 6 | import org.springframework.data.mongodb.core.index.Indexed; 7 | 8 | @Data 9 | public class PriceCheckCacheItem { 10 | 11 | @Id 12 | private String id; 13 | @Indexed 14 | private String requestId; 15 | private BasePriceCheckResponse response; 16 | 17 | } -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/web/MainController.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.web; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.servlet.view.RedirectView; 6 | import springfox.documentation.annotations.ApiIgnore; 7 | 8 | @ApiIgnore 9 | @Controller 10 | public class MainController { 11 | 12 | @RequestMapping(value = "/") 13 | public String index() { 14 | return "index.html"; 15 | } 16 | 17 | @RequestMapping(value = "/temp") 18 | public RedirectView temp() { 19 | return new RedirectView("/"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/ui/component/fileUploader/FileUploader.scss: -------------------------------------------------------------------------------- 1 | .filter-wrapper { 2 | .filters-title { 3 | font-size: 10px; 4 | color: white; 5 | } 6 | .filter-button.p-highlight { 7 | background-color: darkslategrey !important; 8 | } 9 | 10 | .filter-button, .file-upload { 11 | border-radius: 0; 12 | } 13 | 14 | .file-upload { 15 | > .p-fileupload-buttonbar { 16 | border-radius: 0; 17 | } 18 | 19 | > .p-fileupload-content { 20 | border-radius: 0; 21 | } 22 | } 23 | display: flex; 24 | justify-content: center; 25 | flex-wrap: wrap; 26 | } 27 | 28 | .p-progress-spinner { 29 | display: flex; 30 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fallout 76 trade server application 2 | 3 | # Setup 4 | 5 | Run `mvn clean package` to build both backend and front-end 6 | 7 | ## Running 8 | 9 | 1. Setup system properties: 10 | 11 | MongoDB config: 12 | 13 | `mongo.user` - db user 14 | 15 | `mongo.password` - db password 16 | 17 | `mongo.db` - db name 18 | 19 | `mongo.url` - db url 20 | 21 | e.g. `-Dmongo.user= -Dmongo.password= -Dmongo.db= -Dmongo.url=` 22 | 23 | 2. Run Main class 24 | 25 | 3. [Optional] Run `watch` script in `package.json` to watch changes in `ui/` folder and re-build frontend automatically 26 | 27 | 3. Visit http://localhost:8080/ 28 | -------------------------------------------------------------------------------- /src/main/ui/service/localStorage.service.ts: -------------------------------------------------------------------------------- 1 | export const LOCAL_STORAGE_KEYS = { 2 | TOKEN: 'fo76marketToken', 3 | TABLE_SETTINGS: 'fo76tableSettings', 4 | TABLE_COLUMNS: 'fo76tableColumns' 5 | } 6 | 7 | class LocalStorageService { 8 | get(key: string): string { 9 | return localStorage.getItem(key); 10 | } 11 | 12 | set(key: string, value: string): void { 13 | localStorage.setItem(key, value); 14 | } 15 | 16 | delete(key: string): void { 17 | localStorage.removeItem(key); 18 | } 19 | 20 | getToken(): string { 21 | return this.get(LOCAL_STORAGE_KEYS.TOKEN); 22 | } 23 | } 24 | 25 | export const localStorageService = new LocalStorageService(); -------------------------------------------------------------------------------- /src/main/ui/component/config/BaseInventOmatic.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | text-align: center; 3 | font-size: 20px; 4 | color: rgba(255, 255, 255, 0.6); 5 | * > .mod-selector { 6 | font-size: 15px; 7 | margin: 20px; 8 | } 9 | * > .MuiFormControl-root > * { 10 | font-size: 14px; 11 | } 12 | .mod-config-form { 13 | display: inline-flex; 14 | justify-content: center; 15 | inline-size: fit-content; 16 | contain: content; 17 | * > .MuiGrid-spacing-xs-2 { 18 | margin-top: 40px; 19 | } 20 | } 21 | } 22 | 23 | * > .space { 24 | margin: 20px; 25 | } 26 | 27 | * > .title { 28 | margin: 20px; 29 | } 30 | 31 | .break { 32 | flex-basis: 100%; 33 | height: 0; 34 | } -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/domain/config/Fed76Config.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.domain.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | @Data 9 | public class Fed76Config { 10 | 11 | @Value("${fed76.mapping.url}") 12 | private String mappingUrl; 13 | @Value("${fed76.plan.pricing.url}") 14 | private String planPricingUrl; 15 | @Value("${fed76.item.pricing.url}") 16 | private String itemPricingUrl; 17 | @Value("${fed76.price.enhance.url}") 18 | private String priceEnhanceUrl; 19 | @Value("${fed76.price.check.use_id}") 20 | private boolean useIdForPriceCheck; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/domain/config/MongoDbConfig.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.domain.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Data 9 | @Component 10 | @ConfigurationProperties(prefix = "mongo-config") 11 | public class MongoDbConfig { 12 | @Value("${user}") 13 | private String user; 14 | @Value("${cluster}") 15 | private String cluster; 16 | @Value("${db}") 17 | private String db; 18 | @Value("${password}") 19 | private String password; 20 | @Value("${fullUrl}") 21 | private String fullUrl; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/config/Swagger2UiConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 6 | import springfox.documentation.builders.RequestHandlerSelectors; 7 | import springfox.documentation.spi.DocumentationType; 8 | import springfox.documentation.spring.web.plugins.Docket; 9 | import springfox.documentation.swagger2.annotations.EnableSwagger2; 10 | 11 | @Configuration 12 | @EnableSwagger2 13 | public class Swagger2UiConfiguration implements WebMvcConfigurer { 14 | 15 | 16 | @Bean 17 | public Docket api() { 18 | return new Docket(DocumentationType.SWAGGER_2).select() 19 | .apis(RequestHandlerSelectors.any()) 20 | .build(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/web/GenericErrorController.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.web; 2 | 3 | import javax.servlet.RequestDispatcher; 4 | import javax.servlet.http.HttpServletRequest; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.boot.web.servlet.error.ErrorController; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import springfox.documentation.annotations.ApiIgnore; 10 | 11 | @ApiIgnore 12 | @Controller 13 | @Slf4j 14 | public class GenericErrorController implements ErrorController { 15 | 16 | @RequestMapping("/error") 17 | public String error(HttpServletRequest request) { 18 | log.error("Error code: {}", request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE)); 19 | return "error.html"; 20 | } 21 | 22 | @Override 23 | public String getErrorPath() { 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/web/api/FilterFlagResponse.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.web.api; 2 | 3 | import com.manson.domain.fo76.items.enums.FilterFlag; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | import lombok.Data; 7 | 8 | @Data 9 | public class FilterFlagResponse { 10 | 11 | private final String name; 12 | private final List flags; 13 | private final String value; 14 | private final boolean hasStarMods; 15 | private final List subtypes; 16 | private final boolean parent; 17 | 18 | public FilterFlagResponse(FilterFlag filterFlag) { 19 | this.value = filterFlag.getValue(); 20 | this.flags = filterFlag.getFlags(); 21 | this.hasStarMods = filterFlag.isHasStarMods(); 22 | this.subtypes = filterFlag.getSubtypes().stream().map(FilterFlagResponse::new).collect(Collectors.toList()); 23 | this.name = filterFlag.name(); 24 | this.parent = filterFlag.isParent(); 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/test/resources/hibernate.cfg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | false 8 | false 9 | false 10 | true 11 | org.hibernate.dialect.SQLiteDialect 12 | org.sqlite.JDBC 13 | jdbc:sqlite:D:\workspace\fo76tradeServer\db.slqlite 14 | 15 | validate 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/ui/service/base.service.ts: -------------------------------------------------------------------------------- 1 | import {localStorageService} from "./localStorage.service"; 2 | 3 | export class BaseService { 4 | protected readonly baseEndPoint: string; 5 | 6 | constructor(baseEndPoint: string) { 7 | this.baseEndPoint = baseEndPoint; 8 | } 9 | 10 | protected performRequest = ({url, method, data}: { url: string, method: string, data?: any }): any => { 11 | const params = { 12 | method, 13 | headers: { 14 | 'Accept': 'application/json', 15 | 'Content-Type': 'application/json', 16 | } 17 | }; 18 | if (data) { 19 | // @ts-ignore 20 | params.body = JSON.stringify(data); 21 | } 22 | const token = localStorageService.getToken(); 23 | if (token !== null && token !== undefined && token.length > 0) { 24 | // @ts-ignore 25 | params.headers['Authorization'] = `Bearer ${token}`; 26 | } 27 | return fetch(url, params).then((resp) => resp.json(), (e) => { 28 | console.error(e); 29 | }); 30 | }; 31 | } -------------------------------------------------------------------------------- /src/main/ui/component/config/HUDEditor.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | text-align: center; 3 | font-size: 20px; 4 | color: rgba(255, 255, 255, 0.6); 5 | 6 | > .hud-editor-form { 7 | > .title { 8 | text-decoration: none; 9 | } 10 | > .elements-row { 11 | display: flex; 12 | flex-direction: row; 13 | flex-wrap: wrap; 14 | justify-content: center; 15 | 16 | & .hud { 17 | width: 20px; 18 | } 19 | 20 | > .hud-elements { 21 | //border: 2px solid blue; 22 | > .tooltip { 23 | font-size: 30px; 24 | } 25 | width: 15rem; 26 | margin: 1rem; 27 | display: flex; 28 | flex-direction: column; 29 | 30 | > .title { 31 | font-size: 14px; 32 | } 33 | 34 | > .description { 35 | font-size: 13px; 36 | word-break: break-all; 37 | } 38 | 39 | > .hud-element { 40 | > .element-label { 41 | font-size: 11px; 42 | } 43 | } 44 | } 45 | } 46 | 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/service/AppInfo.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.service; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.manson.fo76.domain.config.GitConfig; 5 | import java.util.List; 6 | import javax.annotation.PostConstruct; 7 | import lombok.Data; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.context.properties.ConfigurationProperties; 10 | import org.springframework.stereotype.Component; 11 | 12 | @Data 13 | @Component 14 | @ConfigurationProperties("app") 15 | public class AppInfo { 16 | 17 | private String name; 18 | 19 | private String version; 20 | 21 | @JsonIgnore 22 | private String commitUrlFormat; 23 | 24 | private List sites; 25 | 26 | private List tools; 27 | 28 | private String discord; 29 | 30 | private String github; 31 | 32 | private String commitUrl; 33 | 34 | @Autowired 35 | private GitConfig gitConfig; 36 | 37 | @PostConstruct 38 | public void init() { 39 | this.commitUrl = this.commitUrlFormat + this.gitConfig.getGitCommitId(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/service/JsonParser.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.service; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.manson.fo76.config.AppConfig; 6 | import java.util.Map; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | @Slf4j 10 | public class JsonParser { 11 | 12 | private static final TypeReference> TYPE_REFERENCE = new TypeReference>() { 13 | }; 14 | private static final ObjectMapper OM = AppConfig.getObjectMapper(); 15 | 16 | public static Map objectToMap(Object o) { 17 | try { 18 | return OM.convertValue(o, TYPE_REFERENCE); 19 | } catch (Exception e) { 20 | log.error("Error converting object to map: {}", o, e); 21 | } 22 | return null; 23 | } 24 | 25 | public static T mapToClass(Map map, Class clazz) { 26 | try { 27 | return OM.convertValue(map, clazz); 28 | } catch (Exception e) { 29 | log.error("Converting map to class: {}", map, e); 30 | } 31 | return null; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/service/BaseRestClient.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.service; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.jayway.jsonpath.spi.json.JacksonJsonProvider; 5 | import javax.ws.rs.client.Client; 6 | import javax.ws.rs.client.ClientBuilder; 7 | import org.glassfish.jersey.client.ClientConfig; 8 | import org.glassfish.jersey.media.multipart.MultiPartFeature; 9 | import org.glassfish.jersey.media.multipart.internal.MultiPartWriter; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | 13 | @Service 14 | public class BaseRestClient { 15 | 16 | protected Client client; 17 | protected ObjectMapper objectMapper; 18 | 19 | @Autowired 20 | public BaseRestClient(ObjectMapper objectMapper) { 21 | this.objectMapper = objectMapper; 22 | ClientConfig clientConfig = new ClientConfig(); 23 | clientConfig.register(new JacksonJsonProvider(objectMapper)); 24 | clientConfig.register(MultiPartFeature.class); 25 | clientConfig.register(MultiPartWriter.class); 26 | this.client = ClientBuilder.newClient(clientConfig); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker image, publish to Heroku & Azure 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Login to Docker Hub 14 | uses: docker/login-action@v1 15 | with: 16 | username: ${{ secrets.DOCKER_NAME }} 17 | password: ${{ secrets.DOCKER_PASS }} 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 1.8 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 1.8 23 | - name: Build Docker image 24 | run: mvn -B clean git-commit-id:revision spring-boot:build-image -Dspring-boot.build-image.imageName=mansonew2/fo76market -DskipTests=true --file pom.xml 25 | - name: Publish the Docker image 26 | run: docker push mansonew2/fo76market:latest 27 | - name: Deploy to Azure Web App 28 | uses: azure/webapps-deploy@v2 29 | with: 30 | app-name: 'fo76market' 31 | slot-name: 'production' 32 | publish-profile: ${{ secrets.AzureAppService_PublishProfile_9a289aaa1e784de39e570572274e7797 }} 33 | package: '${{ github.workspace }}/target/*.jar' 34 | -------------------------------------------------------------------------------- /src/main/ui/service/item.service.ts: -------------------------------------------------------------------------------- 1 | import {ModDataRequest, ReportItem} from "./domain"; 2 | import {BaseService} from "./base.service"; 3 | import {ItemContext} from "./filter.service"; 4 | 5 | class ItemService extends BaseService { 6 | 7 | constructor() { 8 | super('items/'); 9 | this.prepareModData = this.prepareModData.bind(this); 10 | this.reportItem = this.reportItem.bind(this); 11 | } 12 | 13 | prepareModData(modDataRequest: ModDataRequest, itemContext: ItemContext) { 14 | let requestParams = ''; 15 | Object.keys(itemContext).forEach((k: string) => { 16 | // @ts-ignore 17 | requestParams += k + '=' + itemContext[k] + '&'; 18 | }); 19 | const finalUrl = `${this.baseEndPoint}prepareModData?${requestParams}`; 20 | return this.performRequest({ 21 | url: finalUrl, 22 | method: 'POST', 23 | data: modDataRequest, 24 | }); 25 | } 26 | 27 | reportItem(item: ReportItem): Promise { 28 | const finalUrl = `${this.baseEndPoint}report`; 29 | return this.performRequest({ 30 | url: finalUrl, 31 | method: 'POST', 32 | data: item 33 | }) 34 | } 35 | } 36 | 37 | export const itemService = new ItemService(); -------------------------------------------------------------------------------- /src/test/java/com/fo76/jdump/domain/FormId.java: -------------------------------------------------------------------------------- 1 | package com.fo76.jdump.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.manson.domain.AbstractObject; 5 | 6 | public class FormId extends AbstractObject { 7 | 8 | @JsonProperty("formid") 9 | private String formId; 10 | private String signature; 11 | @JsonProperty("editorid") 12 | private String editorId; 13 | private String name; 14 | @JsonProperty("@type") 15 | private String type; 16 | 17 | public String getFormId() { 18 | return formId; 19 | } 20 | 21 | public void setFormId(String formId) { 22 | this.formId = formId; 23 | } 24 | 25 | public String getSignature() { 26 | return signature; 27 | } 28 | 29 | public void setSignature(String signature) { 30 | this.signature = signature; 31 | } 32 | 33 | public String getEditorId() { 34 | return editorId; 35 | } 36 | 37 | public void setEditorId(String editorId) { 38 | this.editorId = editorId; 39 | } 40 | 41 | public String getName() { 42 | return name; 43 | } 44 | 45 | public void setName(String name) { 46 | this.name = name; 47 | } 48 | 49 | public String getType() { 50 | return type; 51 | } 52 | 53 | public void setType(String type) { 54 | this.type = type; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/ui/service/game.api.service.ts: -------------------------------------------------------------------------------- 1 | import {BaseService} from "./base.service"; 2 | import {AppInfo, Item, PriceCheckResponse} from "./domain"; 3 | import {FilterFlag} from "./filter.service"; 4 | import {HUDEditorSchema} from "../component/config/HUDEditor"; 5 | 6 | class GameApiService extends BaseService { 7 | 8 | constructor() { 9 | super('game/'); 10 | } 11 | 12 | priceCheck(item: Item): Promise { 13 | const finalUrl = `${this.baseEndPoint}priceCheck`; 14 | return this.performRequest({ 15 | url: finalUrl, 16 | method: 'POST', 17 | data: item, 18 | }); 19 | } 20 | 21 | filterFlags(): Promise> { 22 | const finalUrl = `${this.baseEndPoint}filterFlags`; 23 | return this.performRequest({ 24 | url: finalUrl, 25 | method: 'GET', 26 | }); 27 | } 28 | 29 | appInfo(): Promise { 30 | const finalUrl = `${this.baseEndPoint}appInfo`; 31 | return this.performRequest({ 32 | url: finalUrl, 33 | method: 'GET', 34 | }); 35 | } 36 | 37 | hudEditorConfig(): Promise { 38 | const finalUrl = `${this.baseEndPoint}hudEditorConfig`; 39 | return this.performRequest({ 40 | url: finalUrl, 41 | method: 'GET', 42 | }); 43 | } 44 | } 45 | 46 | export const gameApiService = new GameApiService(); -------------------------------------------------------------------------------- /src/test/java/com/fo76/NukaCryptService.kt: -------------------------------------------------------------------------------- 1 | package com.fo76 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.manson.domain.nuka.NukaRequest 5 | import com.manson.domain.nuka.NukaResponse 6 | import com.manson.fo76.service.BaseRestClient 7 | import java.io.BufferedReader 8 | import java.io.InputStream 9 | import javax.ws.rs.client.Entity 10 | import javax.ws.rs.client.WebTarget 11 | import javax.ws.rs.core.MediaType 12 | import org.glassfish.jersey.media.multipart.FormDataMultiPart 13 | import org.springframework.stereotype.Service 14 | 15 | @Service 16 | class NukaCryptService(objectMapper: ObjectMapper) : BaseRestClient(objectMapper) { 17 | 18 | fun performRequest(request: NukaRequest): NukaResponse? { 19 | val webResource: WebTarget = client 20 | .target("https://nukacrypt.com/queries/searchdata.php") 21 | val multiPart = FormDataMultiPart() 22 | val map = request.toMultiPartMap() 23 | map.forEach { (k, v) -> 24 | multiPart.field(k, v) 25 | } 26 | val response = webResource.request().accept(MediaType.MULTIPART_FORM_DATA) 27 | .buildPost(Entity.entity(multiPart, MediaType.MULTIPART_FORM_DATA)).invoke() 28 | val responseString = BufferedReader((response.entity as InputStream).reader()).readText() 29 | return objectMapper.readValue(responseString, NukaResponse::class.java) 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/helper/JFlatCustom.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.helper; 2 | 3 | import com.github.opendevl.JFlat; 4 | import java.io.Writer; 5 | 6 | public class JFlatCustom extends JFlat { 7 | 8 | public JFlatCustom(String jsonString) { 9 | super(jsonString); 10 | } 11 | 12 | @Override 13 | public JFlatCustom json2Sheet() { 14 | return (JFlatCustom) super.json2Sheet(); 15 | } 16 | 17 | @Override 18 | public JFlatCustom headerSeparator() throws Exception { 19 | return (JFlatCustom) super.headerSeparator(); 20 | } 21 | 22 | @Override 23 | public JFlatCustom headerSeparator(String separator) throws Exception { 24 | return (JFlatCustom) super.headerSeparator(separator); 25 | } 26 | 27 | public void write2csv(Writer writer, Character delimiter) { 28 | try { 29 | boolean comma; 30 | for (Object[] o : this.getJsonAsSheet()) { 31 | comma = false; 32 | for (Object t : o) { 33 | if (t == null) { 34 | writer.write(comma ? delimiter.toString() : ""); 35 | } else { 36 | writer.write(comma ? delimiter + t.toString() : t.toString()); 37 | } 38 | if (!comma) { 39 | comma = true; 40 | } 41 | } 42 | writer.write(System.lineSeparator()); 43 | } 44 | writer.close(); 45 | } catch (Exception e) { 46 | e.printStackTrace(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/ui/component/dialog/InfoDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Dialog} from "primereact/dialog"; 3 | import {Button} from "primereact/button"; 4 | 5 | export class InfoDialog extends React.Component { 6 | 7 | state = { 8 | visible: false, 9 | content: '', 10 | header: '' 11 | } 12 | 13 | private setVisible(value: boolean) { 14 | this.setState({visible: value}); 15 | } 16 | 17 | private setContent(value: any) { 18 | this.setState({content: value}); 19 | } 20 | 21 | private setHeader(value: any) { 22 | this.setState({header: value}); 23 | } 24 | 25 | public show(header: any, content: any) { 26 | this.setContent(content); 27 | this.setHeader(header); 28 | this.setVisible(true); 29 | } 30 | 31 | private renderFooter() { 32 | return ( 33 |
34 |
38 | ); 39 | } 40 | 41 | render() { 42 | const {header, content, visible} = this.state; 43 | return ( 44 | { 48 | this.setVisible(false) 49 | }} 50 | footer={this.renderFooter()}> 51 | {content} 52 | 53 | ); 54 | } 55 | } -------------------------------------------------------------------------------- /src/test/java/com/fo76/jdump/domain/JDumpEntry.java: -------------------------------------------------------------------------------- 1 | package com.fo76.jdump.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.manson.domain.AbstractObject; 5 | import java.util.List; 6 | 7 | public class JDumpEntry extends AbstractObject { 8 | 9 | @JsonProperty("Record Header") 10 | private RecordHeader recordHeader; 11 | 12 | @JsonProperty("EDID") 13 | private String edid; 14 | @JsonProperty("DURL") 15 | private String durl; 16 | @JsonProperty("XALG") 17 | private String xalg; 18 | @JsonProperty("FULL") 19 | private String full; 20 | 21 | @JsonProperty("DESC") 22 | private Object desc; 23 | 24 | @JsonProperty("Model") 25 | private Model model; 26 | 27 | @JsonProperty("INDX") 28 | private String indx; 29 | 30 | // @JsonProperty("DATA") 31 | // private Data data; 32 | 33 | @JsonProperty("MNAM") 34 | private List mnam; 35 | 36 | @JsonProperty("LNAM") 37 | private FormId lnam; 38 | 39 | @JsonProperty("NAM1") 40 | private String nam1; 41 | 42 | @JsonProperty("NAM2") 43 | private Object nam2; 44 | 45 | @JsonProperty("NAM3") 46 | private List nam3; 47 | 48 | @JsonProperty("OBST") 49 | private Object obst; 50 | 51 | @JsonProperty("ARTC") 52 | private FormId artc; 53 | 54 | @JsonProperty("DNAM") 55 | private FormId dnam; 56 | 57 | @JsonProperty("ENAM") 58 | private FormId enam; 59 | 60 | @JsonProperty("FNAM") 61 | private List fnam; 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/fo76/XTranslatorParser.kt: -------------------------------------------------------------------------------- 1 | package com.fo76 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.github.underscore.lodash.U 6 | import com.manson.domain.config.Fo76String 7 | import java.io.File 8 | import java.nio.file.Files 9 | import java.nio.file.Paths 10 | import java.util.function.Consumer 11 | import org.springframework.beans.factory.annotation.Autowired 12 | import org.springframework.stereotype.Service 13 | 14 | @Service 15 | class XTranslatorParser(@Autowired private val objectMapper: ObjectMapper) { 16 | fun parse(file: File): List { 17 | try { 18 | var xml = java.lang.String.join("\n", Files.readAllLines(Paths.get(file.toURI()))) 19 | xml = xml.replace(xml[0].toString(), "") 20 | val map = U.fromXml>(xml) 21 | val sstxmlRessources = map["SSTXMLRessources"] as Map<*, *>? 22 | val lang = (sstxmlRessources!!["Params"] as Map<*, *>?)!!["Dest"] as String? 23 | val strings = (sstxmlRessources["Content"] as Map<*, *>?)!!["String"] as List<*> 24 | val typeReference: TypeReference> = object : TypeReference>() {} 25 | val fo76Strings = objectMapper.convertValue(strings, typeReference) 26 | fo76Strings.forEach(Consumer { str: Fo76String -> str.lang = lang!! }) 27 | return fo76Strings 28 | } catch (e: Exception) { 29 | e.printStackTrace() 30 | } 31 | return emptyList() 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/service/ItemService.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.service; 2 | 3 | 4 | import com.manson.domain.fo76.items.enums.FilterFlag; 5 | import com.manson.domain.itemextractor.ItemConfig; 6 | import com.manson.domain.itemextractor.ItemResponse; 7 | import com.manson.domain.itemextractor.ModDataRequest; 8 | import com.manson.fo76.domain.ItemContext; 9 | import com.manson.fo76.domain.ReportedItem; 10 | import com.manson.fo76.repository.ReportedItemRepository; 11 | import java.util.Date; 12 | import java.util.List; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.stereotype.Service; 16 | 17 | @Service 18 | @Slf4j 19 | public class ItemService { 20 | 21 | private final ItemConverterService itemConverterService; 22 | 23 | private final ReportedItemRepository reportedItemRepository; 24 | 25 | @Autowired 26 | public ItemService(ItemConverterService itemConverterService, ReportedItemRepository reportedItemRepository) { 27 | this.itemConverterService = itemConverterService; 28 | this.reportedItemRepository = reportedItemRepository; 29 | } 30 | 31 | public ReportedItem reportItem(ReportedItem item) { 32 | item.setDate(new Date()); 33 | return reportedItemRepository.save(item); 34 | } 35 | 36 | public List prepareModData(ModDataRequest request, ItemContext context) { 37 | return itemConverterService.prepareModData(request, context); 38 | } 39 | 40 | public List getAllReportedItems() { 41 | return reportedItemRepository.findAll(); 42 | } 43 | 44 | public ItemConfig findItemConfig(String name, FilterFlag filterFlag) { 45 | return itemConverterService.findItemConfig(name, filterFlag); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/ui/component/config/configs.ts: -------------------------------------------------------------------------------- 1 | const stringIsNumber = (value: any) => !isNaN(Number(value)); 2 | 3 | const toArray = (enumme: any) => Object.keys(enumme).filter(stringIsNumber).map(key => enumme[key]); 4 | 5 | export enum ItemType { 6 | ALL, 7 | POWER_ARMOR, 8 | WEAPON, 9 | ARMOR, 10 | APPAREL, 11 | FOOD_WATER, 12 | AID, 13 | NOTES, 14 | HOLO, 15 | AMMO, 16 | MISC, 17 | MODS, 18 | JUNK 19 | } 20 | 21 | export enum MatchMode { 22 | ALL, 23 | EXACT, 24 | CONTAINS, 25 | STARTS, 26 | } 27 | 28 | enum Action { 29 | CONSUME, 30 | DROP, 31 | TRANSFER, 32 | SCRAP 33 | } 34 | 35 | export interface TeenoodleTragedyProtection { 36 | ignoreLegendaries: boolean, 37 | ignoreNonTradable: boolean 38 | } 39 | 40 | export interface ItemNameConfig { 41 | name: string, 42 | quantity: number, 43 | matchMode: keyof MatchMode, 44 | enabled: boolean, 45 | type: keyof ItemType, 46 | action: keyof Action 47 | } 48 | 49 | export interface SectionConfig { 50 | name: string, 51 | hotkey: number, 52 | _comment?: string, 53 | itemNames: Array, 54 | enabled: boolean, 55 | checkCharacterName: boolean, 56 | characterName?: string, 57 | teenoodleTragedyProtection?: TeenoodleTragedyProtection 58 | } 59 | 60 | export interface ActionSectionConfig { 61 | enabled: boolean, 62 | configs: Array 63 | } 64 | 65 | export interface InventOmaticPipboyConfig { 66 | debug: boolean, 67 | showRealItemName: boolean, 68 | actions: Array 69 | } 70 | 71 | export const MatchModes = toArray(MatchMode); 72 | export const ItemTypes = toArray(ItemType); 73 | const Actions = toArray(Action); 74 | export const StashActions = Actions.filter(x => 'CONSUME' !== x).filter(x => 'DROP' !== x); 75 | export const PipboyActions = Actions.filter(x => 'SCRAP' !== x).filter(x => 'TRANSFER' !== x); -------------------------------------------------------------------------------- /src/main/ui/service/tableSettings.service.ts: -------------------------------------------------------------------------------- 1 | import {LOCAL_STORAGE_KEYS, localStorageService} from "./localStorage.service"; 2 | import {ColumnDefinition, columns, createColumnDef, SimpleColumn,} from "./table.columns"; 3 | 4 | class TableSettingsService { 5 | 6 | private columnsInUse: Array = [...columns]; 7 | 8 | public save(obj: Array) { 9 | localStorageService.set(LOCAL_STORAGE_KEYS.TABLE_COLUMNS, JSON.stringify(obj)); 10 | } 11 | 12 | private static load(): Array { 13 | try { 14 | return JSON.parse(localStorageService.get(LOCAL_STORAGE_KEYS.TABLE_COLUMNS)); 15 | } catch (e) { 16 | console.error('Error loading table columns'); 17 | } 18 | return []; 19 | } 20 | 21 | public saveNewColumn(obj: SimpleColumn) { 22 | const savedColumns = TableSettingsService.load(); 23 | savedColumns.push(obj); 24 | this.save(savedColumns); 25 | } 26 | 27 | public getAllSavedColumns(): Array { 28 | const savedColumns = TableSettingsService.load(); 29 | const newColumns: Array = []; 30 | 31 | if (savedColumns && savedColumns.length > 0) { 32 | savedColumns.forEach((col: ColumnDefinition) => { 33 | if (!columns.some(e => e.field === col.field)) { 34 | newColumns.push(createColumnDef({...col})); 35 | } 36 | }); 37 | } 38 | return newColumns; 39 | } 40 | 41 | 42 | public getAllColumns(): Array { 43 | const newColumns: Array = this.getAllSavedColumns(); 44 | this.columnsInUse = [...columns, ...newColumns]; 45 | return this.columnsInUse; 46 | } 47 | 48 | public getColumns(): Array { 49 | return this.columnsInUse; 50 | } 51 | 52 | } 53 | 54 | export const tableSettingsService = new TableSettingsService(); 55 | tableSettingsService.getAllColumns(); -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/domain/fed76/PriceEnhanceRequest.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.domain.fed76; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 6 | 7 | import com.manson.domain.config.ArmorConfig; 8 | import com.manson.domain.fed76.pricing.LegendaryMod; 9 | import com.manson.domain.fed76.pricing.VendorData; 10 | import com.manson.domain.fo76.items.enums.ArmorGrade; 11 | import java.util.List; 12 | import java.util.Objects; 13 | import lombok.Data; 14 | import lombok.EqualsAndHashCode; 15 | import lombok.NoArgsConstructor; 16 | import lombok.ToString; 17 | import org.apache.commons.collections4.CollectionUtils; 18 | import org.apache.commons.lang3.StringUtils; 19 | 20 | @Data 21 | @ToString 22 | @EqualsAndHashCode 23 | @NoArgsConstructor 24 | public class PriceEnhanceRequest { 25 | 26 | @JsonInclude(Include.ALWAYS) 27 | private String itemName; 28 | @JsonInclude(Include.ALWAYS) 29 | private String gameId; 30 | @JsonInclude(Include.ALWAYS) 31 | private VendorData vendingData; 32 | @JsonInclude(Include.ALWAYS) 33 | private ArmorConfig armorConfig; 34 | @JsonInclude(Include.ALWAYS) 35 | private List legendaryMods; 36 | @JsonInclude(Include.ALWAYS) 37 | private boolean isLegendary; 38 | 39 | @JsonIgnore 40 | public boolean isValid() { 41 | boolean validPrice = Objects.nonNull(this.vendingData) && Objects.nonNull(this.vendingData.getPrice()) 42 | && this.vendingData.getPrice() > 0; 43 | boolean validMods = CollectionUtils.isNotEmpty(this.legendaryMods) && this.legendaryMods.stream() 44 | .allMatch(x -> StringUtils.isNotBlank(x.getGameId())); 45 | boolean validConfig = true; 46 | if (Objects.nonNull(this.armorConfig) && armorConfig.getArmorGrade() != ArmorGrade.Unknown) { 47 | validConfig = StringUtils.isNoneBlank(armorConfig.getArmorId()); 48 | } 49 | 50 | return validConfig && validMods && validPrice; 51 | } 52 | } -------------------------------------------------------------------------------- /src/test/java/com/fo76/jdump/domain/RecordHeader.java: -------------------------------------------------------------------------------- 1 | package com.fo76.jdump.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.manson.domain.AbstractObject; 5 | 6 | public class RecordHeader extends AbstractObject { 7 | 8 | @JsonProperty("Signature") 9 | private String signature; 10 | @JsonProperty("Data Size") 11 | private String dataSize; 12 | 13 | // TODO: string or empty object? 14 | @JsonProperty("Record Flags") 15 | private Object recordFlags; 16 | 17 | @JsonProperty("FormID") 18 | private FormId formId; 19 | 20 | @JsonProperty("Version Control Info 1") 21 | private String versionControlInfo1; 22 | @JsonProperty("Form Version") 23 | private String formVersion; 24 | @JsonProperty("Version Control Info 2") 25 | private String versionControlInfo2; 26 | 27 | public String getSignature() { 28 | return signature; 29 | } 30 | 31 | public void setSignature(String signature) { 32 | this.signature = signature; 33 | } 34 | 35 | public String getDataSize() { 36 | return dataSize; 37 | } 38 | 39 | public void setDataSize(String dataSize) { 40 | this.dataSize = dataSize; 41 | } 42 | 43 | public Object getRecordFlags() { 44 | return recordFlags; 45 | } 46 | 47 | public void setRecordFlags(Object recordFlags) { 48 | this.recordFlags = recordFlags; 49 | } 50 | 51 | public FormId getFormId() { 52 | return formId; 53 | } 54 | 55 | public void setFormId(FormId formId) { 56 | this.formId = formId; 57 | } 58 | 59 | public String getVersionControlInfo1() { 60 | return versionControlInfo1; 61 | } 62 | 63 | public void setVersionControlInfo1(String versionControlInfo1) { 64 | this.versionControlInfo1 = versionControlInfo1; 65 | } 66 | 67 | public String getFormVersion() { 68 | return formVersion; 69 | } 70 | 71 | public void setFormVersion(String formVersion) { 72 | this.formVersion = formVersion; 73 | } 74 | 75 | public String getVersionControlInfo2() { 76 | return versionControlInfo2; 77 | } 78 | 79 | public void setVersionControlInfo2(String versionControlInfo2) { 80 | this.versionControlInfo2 = versionControlInfo2; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import webpack from "webpack"; 3 | 4 | const outputPath = './src/main/resources/static/built/'; 5 | 6 | const config: webpack.Configuration = { 7 | entry: "./src/main/ui/index.tsx", 8 | devtool: false, 9 | mode: 'development', 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(ts|js)x?$/, 14 | exclude: /node_modules/, 15 | use: { 16 | loader: "babel-loader", 17 | options: { 18 | presets: [ 19 | "@babel/preset-env", 20 | "@babel/preset-react", 21 | "@babel/preset-typescript", 22 | ], 23 | }, 24 | }, 25 | }, 26 | { 27 | test: /\.tsx?$/, 28 | exclude: /node_modules/, 29 | use: { 30 | loader: 'ts-loader', 31 | } 32 | }, 33 | { 34 | test: /\.(png)(\?|$)/, 35 | use: { 36 | loader: 'url-loader?limit=100000' 37 | } 38 | }, 39 | { 40 | test: /\.(svg|ttf|woff|woff2|eot)$/, 41 | loader: 'url-loader', 42 | options: { 43 | name: '[name].[ext]?[hash]', 44 | }, 45 | }, 46 | { 47 | test: /\.(sass|scss|css)$/i, 48 | use: [ 49 | 'style-loader', 50 | 'css-loader', 51 | 'sass-loader', 52 | ], 53 | }, 54 | ], 55 | }, 56 | resolve: { 57 | extensions: [".tsx", ".ts", ".js"], 58 | fallback: { 59 | "buffer": false, 60 | "http": false, 61 | "https": false, 62 | 63 | "fs": false, 64 | "tls": false, 65 | "net": false, 66 | "path": false, 67 | "zlib": false, 68 | "stream": require.resolve("stream-browserify"), 69 | "crypto": false, 70 | "process": false 71 | } 72 | }, 73 | plugins: [ 74 | new webpack.ProvidePlugin({ 75 | process: 'process/browser', 76 | }), 77 | ], 78 | output: { 79 | path: path.join(__dirname, outputPath), 80 | filename: 'bundle.js', 81 | }, 82 | devServer: { 83 | contentBase: path.join(__dirname, outputPath), 84 | compress: true, 85 | port: 4000, 86 | } 87 | }; 88 | 89 | export default config; -------------------------------------------------------------------------------- /src/main/ui/service/rogue.service.ts: -------------------------------------------------------------------------------- 1 | import {Item} from "./domain"; 2 | 3 | const headers: Array = ["Prefix", "Type", "Major", "Minor", "Level", "Notes", "Value", "Character"]; 4 | const separator = ","; 5 | const eof = "\r\n"; 6 | 7 | interface RogueCSVLine { 8 | prefix?: string, 9 | type?: string, 10 | major?: string, 11 | minor?: string, 12 | level: number, 13 | notes?: string, 14 | value?: number, 15 | character: string, 16 | } 17 | 18 | const legendaryFilter = (object: Item) => { 19 | return object && object.isLegendary && object.isTradable; 20 | } 21 | 22 | export class RogueService { 23 | 24 | private static getLegModValue(item: Item, index: number) { 25 | if (item && item.itemDetails && item.itemDetails.legendaryModConfig && item.itemDetails.legendaryModConfig.legendaryMods.length >= index 26 | && item.itemDetails.legendaryModConfig.legendaryMods[index]) { 27 | return item.itemDetails.legendaryModConfig.legendaryMods[index].gameId; 28 | } 29 | } 30 | 31 | private static getItemName(item: Item) { 32 | // if (item && item.itemDetails && item.itemDetails.config && item.itemDetails.config.gameId) { 33 | // return item.itemDetails.config.gameId; 34 | // } 35 | if (item && item.itemDetails) { 36 | return item.itemDetails.name; 37 | } 38 | } 39 | 40 | private static createRogueObject(item: Item, character: string): RogueCSVLine { 41 | return { 42 | prefix: this.getLegModValue(item, 0), 43 | type: this.getItemName(item), 44 | major: this.getLegModValue(item, 1), 45 | minor: this.getLegModValue(item, 2), 46 | level: item.itemLevel, 47 | notes: '', 48 | value: item.itemValue, 49 | character, 50 | } 51 | } 52 | 53 | private static toRogueObject(data: Array, character: string): Array { 54 | return data.filter(legendaryFilter).map(v => RogueService.createRogueObject(v, character)); 55 | } 56 | 57 | private static toCSVLine(object: any): string { 58 | return Object.values(object).join(separator) + eof; 59 | } 60 | 61 | 62 | public static toCSV(data: Array, character: string): string { 63 | let csv = RogueService.toCSVLine(headers); 64 | let rogueObjects = RogueService.toRogueObject(data, character); 65 | rogueObjects.forEach(value => { 66 | csv += RogueService.toCSVLine(value); 67 | }); 68 | return csv.replace("’", "'").replace("+", "﹢"); 69 | } 70 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fallout76-trade-hub", 3 | "version": "1.0.0", 4 | "description": "Fallout76 trade hub", 5 | "repository": { 6 | "type": "git", 7 | "url": "git@github.com:sdaskaliesku/fo76tradeServer.git" 8 | }, 9 | "author": "Serhii Daskaliesku", 10 | "license": "Apache-2.0", 11 | "bugs": { 12 | "url": "https://github.com/sdaskaliesku/fo76tradeServer/issues" 13 | }, 14 | "homepage": "https://github.com/sdaskaliesku/fo76tradeServer/", 15 | "dependencies": { 16 | "@jsonforms/core": "^2.5.2", 17 | "@jsonforms/material-renderers": "^2.5.2", 18 | "@jsonforms/react": "^2.5.2", 19 | "@material-ui/core": "^4.11.4", 20 | "@material-ui/icons": "^4.11.2", 21 | "fast-xml-parser": "^3.19.0", 22 | "jstoxml": "^2.1.0", 23 | "material-ui-color": "^1.2.0", 24 | "primeicons": "^4.1.0", 25 | "primereact": "^6.5.0-rc.2", 26 | "react": "^17.0.1", 27 | "react-dom": "^17.0.1", 28 | "react-json-view": "^1.21.1", 29 | "react-router-dom": "^5.2.0", 30 | "react-st-modal": "^1.1.3", 31 | "react-textarea-autosize": "^6.1.0", 32 | "react-transition-group": "^4.4.1", 33 | "stream-browserify": "^3.0.0" 34 | }, 35 | "scripts": { 36 | "start": "webpack serve --open", 37 | "build": "webpack --mode production --output-path ./target/classes/static/built/", 38 | "watch": "webpack --watch --output-path ./target/classes/static/built/" 39 | }, 40 | "eslintConfig": { 41 | "extends": [ 42 | "react-app", 43 | "react-app/jest" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.13.1", 48 | "@babel/plugin-transform-runtime": "^7.13.7", 49 | "@babel/preset-env": "^7.13.5", 50 | "@babel/preset-react": "^7.12.13", 51 | "@babel/preset-typescript": "^7.13.0", 52 | "@babel/runtime": "^7.13.7", 53 | "@types/jstoxml": "^2.0.1", 54 | "@types/mui-datatables": "^3.7.0", 55 | "@types/react": "^17.0.2", 56 | "@types/react-dom": "^17.0.1", 57 | "@types/react-router-dom": "^5.1.7", 58 | "@types/webpack": "^4.41.26", 59 | "@types/webpack-dev-server": "^3.11.1", 60 | "@types/xml2js": "^0.4.9", 61 | "babel-loader": "^8.2.2", 62 | "classnames": "^2.2.6", 63 | "css-loader": "^5.0.2", 64 | "node-sass": "^5.0.0", 65 | "sass-loader": "^11.0.1", 66 | "style-loader": "^2.0.0", 67 | "ts-loader": "^8.0.17", 68 | "ts-node": "^9.1.1", 69 | "typescript": "^4.2.2", 70 | "url-loader": "^4.1.1", 71 | "webpack": "^5.24.2", 72 | "webpack-cli": "^4.5.0", 73 | "webpack-dev-server": "^3.11.2" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/ui/component/config/schema-form/Components.tsx: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useState} from "react"; 2 | import {Utils} from "../../../service/utils"; 3 | import {InputNumber} from "primereact/inputnumber"; 4 | import {Checkbox, FormControlLabel} from "@material-ui/core"; 5 | import {Color, ColorPicker, createColor} from 'material-ui-color'; 6 | 7 | export interface CheckboxComponentProps { 8 | label: string 9 | onDataChange: Function, 10 | defaultValue: any 11 | } 12 | 13 | export const CheckboxComponent = (props: CheckboxComponentProps) => { 14 | const {label, onDataChange, defaultValue} = props; 15 | const [value, setValue] = useState(defaultValue); 16 | const key = Utils.uuidv4(); 17 | 18 | const onChange = useCallback( 19 | (e) => { 20 | setValue(e.target.checked); 21 | onDataChange(e.target.checked); 22 | }, 23 | [value] 24 | ); 25 | 26 | return ( 27 |
28 | onChange(e)}/> 31 | } 32 | label={label} 33 | /> 34 |
35 | ) 36 | } 37 | 38 | export const InputNumberComponent = (props: any) => { 39 | const {label, step, min, max, onDataChange, defaultValue} = props; 40 | const [value, setValue] = useState(defaultValue); 41 | const key = Utils.uuidv4(); 42 | 43 | const onChange = (e: any) => { 44 | // setValue(e.value); 45 | onDataChange(e.value); 46 | }; 47 | return ( 48 |
49 | 50 | onChange(e)} 52 | showButtons 53 | step={step} 54 | min={min} max={max} 55 | mode="decimal"/> 56 |
57 | ) 58 | } 59 | 60 | export const ColorPickerComponent = (props: any) => { 61 | const {label, onDataChange, defaultValue} = props; 62 | const [value, setValue] = useState('#' + defaultValue); 63 | const key = Utils.uuidv4(); 64 | 65 | const onChange = useCallback((e: any) => { 66 | if (e.hex) { 67 | onDataChange(e.hex); 68 | } 69 | setValue(e); 70 | }, []); 71 | 72 | return ( 73 |
74 | 75 | 76 |
77 | ) 78 | } -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/service/HudEditorConfigService.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.service; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.google.common.io.Resources; 6 | import java.net.URL; 7 | import java.util.Collections; 8 | import java.util.Map; 9 | import javax.annotation.PostConstruct; 10 | import javax.ws.rs.core.MediaType; 11 | import javax.ws.rs.core.Response; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.apache.commons.collections4.MapUtils; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.stereotype.Service; 16 | 17 | @Service 18 | @Slf4j 19 | public class HudEditorConfigService extends BaseRestClient { 20 | 21 | private static final TypeReference> HUD_EDITOR_CONFIG = new TypeReference>() { 22 | }; 23 | 24 | @Value("${app.config.hud.editor}") 25 | private String url; 26 | 27 | private Map hudEditorConfig; 28 | 29 | @Value("${config.hud.editor.schema}") 30 | private String hudEditorConfigFile; 31 | 32 | public HudEditorConfigService(ObjectMapper objectMapper) { 33 | super(objectMapper); 34 | } 35 | 36 | @PostConstruct 37 | public void init() { 38 | this.hudEditorConfig = loadConfig(objectMapper, hudEditorConfigFile, HUD_EDITOR_CONFIG); 39 | } 40 | 41 | private Map getConfigFromGH() { 42 | try { 43 | String response = this.client.target(url).request(MediaType.APPLICATION_JSON).get(String.class); 44 | return objectMapper.readValue(response, HUD_EDITOR_CONFIG); 45 | } catch (Exception e) { 46 | log.error("Error requesting config", e); 47 | } 48 | return null; 49 | } 50 | 51 | 52 | public Map getConfig() { 53 | Map config = getConfigFromGH(); 54 | if (MapUtils.isEmpty(config)) { 55 | return hudEditorConfig; 56 | } 57 | return config; 58 | } 59 | 60 | private static Map loadConfig(ObjectMapper objectMapper, String file, 61 | TypeReference> typeReference) { 62 | try { 63 | URL resource = getResource(file); 64 | return objectMapper.readValue(resource, typeReference); 65 | } catch (Exception e) { 66 | log.error("Error while loading game config", e); 67 | } 68 | return Collections.emptyMap(); 69 | } 70 | 71 | private static URL getResource(String file) { 72 | try { 73 | //noinspection UnstableApiUsage 74 | return Resources.getResource(file); 75 | } catch (Exception e) { 76 | log.error("Error while loading game config", e); 77 | } 78 | return null; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # game config files 2 | game.config.base.path=configs 3 | game.config.leg.mods.file=${game.config.base.path}/legendaryMods.config.json 4 | game.config.ammo.file=${game.config.base.path}/ammo.types.json 5 | game.config.armor.file=${game.config.base.path}/armor.config.json 6 | game.config.armor.grade.file=${game.config.base.path}/armor.grade.config.json 7 | game.config.weapon.file=${game.config.base.path}/weapons.config.json 8 | game.config.plan.file=${game.config.base.path}/plans.config.json 9 | game.config.armor.name.file=${game.config.base.path}/armor.names.config.json 10 | game.config.weapon.special.file=${game.config.base.path}/special.cases.config.json 11 | game.config.populate.details.all=true 12 | config.hud.editor.schema=${game.config.base.path}/hudeditor.json 13 | 14 | # mongo db config 15 | mongo-config.test.user=fo76test 16 | mongo-config.test.password=test 17 | mongo-config.test.db=test 18 | mongo-config.test.cluster=cluster0.mp0eo.mongodb.net 19 | 20 | mongo-config.user=${mongo.user:${mongo-config.test.user}} 21 | mongo-config.password=${mongo.password:${mongo-config.test.password}} 22 | mongo-config.db=${mongo.db:${mongo-config.test.db}} 23 | mongo-config.cluster=${mongo.url:${mongo-config.test.cluster}} 24 | mongo-config.fullUrl=mongodb+srv://${mongo-config.user}:${mongo-config.password}@${mongo-config.cluster}/${mongo-config.db}?retryWrites=true&w=majority 25 | 26 | # fed76 config 27 | fed76.base.url=https://fed76.info/ 28 | fed76.mapping.url=${fed76.base.url}pricing/mapping 29 | fed76.plan.pricing.url=${fed76.base.url}plan-api 30 | fed76.item.pricing.url=${fed76.base.url}pricing-api 31 | fed76.price.enhance.url=${fed76.base.url}pricing/parse 32 | fed76.price.check.use_id=false 33 | 34 | # app info 35 | app.name=FO76 Trade hub 36 | app.version=@project.version@ 37 | app.sites[0].name=Test 38 | app.sites[0].url=https://fo76market.herokuapp.com/ 39 | app.sites[1].name=Stage 40 | app.sites[1].url=https://fo76market.azurewebsites.net/ 41 | 42 | app.tools[0].name=Invent-O-Matic-Stash 43 | app.tools[0].url=https://www.nexusmods.com/fallout76/mods/698 44 | app.tools[1].name=Invent-O-Matic-Pipboy 45 | app.tools[1].url=https://www.nexusmods.com/fallout76/mods/933 46 | app.tools[2].name=Mod Companion App 47 | app.tools[2].url=https://www.nexusmods.com/fallout76/mods/744 48 | app.tools[3].name=Mod Companion App (Github) 49 | app.tools[3].url=https://github.com/sdaskaliesku/modCompanionApp/packages/414872 50 | 51 | app.discord=https://discord.gg/7fef733 52 | app.github=https://github.com/sdaskaliesku/fo76tradeServer 53 | 54 | app.commitUrlFormat=${app.github}/commit/ 55 | 56 | app.config.hud.editor=https://raw.githubusercontent.com/Annorexorcist/HUDEditor/main/Config_schema/hudeditor.json 57 | 58 | spring.main.banner-mode=off 59 | -------------------------------------------------------------------------------- /src/test/java/com/fo76/jdump/SqlLiteEntity.java: -------------------------------------------------------------------------------- 1 | package com.fo76.jdump; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.Id; 5 | import javax.persistence.Table; 6 | import org.apache.commons.lang3.builder.ToStringBuilder; 7 | import org.apache.commons.lang3.builder.ToStringStyle; 8 | 9 | @Entity 10 | @Table(name = "fo76jdump") 11 | public class SqlLiteEntity { 12 | 13 | @Id 14 | private String formid; 15 | private String signature; 16 | private String editorid; 17 | private String name; 18 | private String desc; 19 | private String flags; 20 | private String version; 21 | private String versioncontrol; 22 | private String jdump; 23 | 24 | public String getFormid() { 25 | return formid; 26 | } 27 | 28 | public void setFormid(String formid) { 29 | this.formid = formid; 30 | } 31 | 32 | public String getSignature() { 33 | return signature; 34 | } 35 | 36 | public void setSignature(String signature) { 37 | this.signature = signature; 38 | } 39 | 40 | public String getEditorid() { 41 | return editorid; 42 | } 43 | 44 | public void setEditorid(String editorid) { 45 | this.editorid = editorid; 46 | } 47 | 48 | public String getName() { 49 | return name; 50 | } 51 | 52 | public void setName(String name) { 53 | this.name = name; 54 | } 55 | 56 | public String getJdump() { 57 | return jdump; 58 | } 59 | 60 | public void setJdump(String jdump) { 61 | this.jdump = jdump; 62 | } 63 | 64 | public String getFlags() { 65 | return flags; 66 | } 67 | 68 | public void setFlags(String flags) { 69 | this.flags = flags; 70 | } 71 | 72 | public String getDesc() { 73 | return desc; 74 | } 75 | 76 | public void setDesc(String desc) { 77 | this.desc = desc; 78 | } 79 | 80 | public String getVersion() { 81 | return version; 82 | } 83 | 84 | public void setVersion(String version) { 85 | this.version = version; 86 | } 87 | 88 | public String getVersioncontrol() { 89 | return versioncontrol; 90 | } 91 | 92 | public void setVersioncontrol(String versioncontrol) { 93 | this.versioncontrol = versioncontrol; 94 | } 95 | 96 | @Override 97 | public String toString() { 98 | return new ToStringBuilder(this, ToStringStyle.NO_CLASS_NAME_STYLE) 99 | .append("formid", formid) 100 | .append("signature", signature) 101 | .append("editorid", editorid) 102 | .append("name", name) 103 | .append("desc", desc) 104 | // .append("flags", flags) 105 | // .append("version", version) 106 | // .append("versioncontrol", versioncontrol) 107 | .toString(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/helper/Utils.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.helper; 2 | 3 | import com.manson.domain.itemextractor.ItemResponse; 4 | import com.manson.fo76.config.AppConfig; 5 | import java.io.StringWriter; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Objects; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.apache.commons.collections4.MapUtils; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.apache.commons.lang3.math.NumberUtils; 13 | 14 | @Slf4j 15 | public final class Utils { 16 | 17 | private static final String DEFAULT_LOCALE = "en"; 18 | 19 | private Utils() { 20 | } 21 | 22 | public static boolean areSameItems(ItemResponse first, ItemResponse second) { 23 | try { 24 | boolean sameName = StringUtils.equalsIgnoreCase(first.getText(), second.getText()); 25 | boolean sameLevel = NumberUtils.compare(first.getItemLevel(), second.getItemLevel()) == 0; 26 | boolean sameLegMods = true; 27 | if (Objects.nonNull(first.getItemDetails()) && Objects.nonNull(second.getItemDetails()) 28 | && Objects.nonNull(first.getItemDetails().getLegendaryModConfig()) 29 | && Objects.nonNull(second.getItemDetails().getLegendaryModConfig())) { 30 | sameLegMods = StringUtils 31 | .equalsIgnoreCase(first.getItemDetails().getLegendaryModConfig().getAbbreviation(), 32 | second.getItemDetails().getLegendaryModConfig().getAbbreviation()); 33 | } 34 | return sameName && sameLevel && sameLegMods; 35 | } catch (Exception ignored) { 36 | return false; 37 | } 38 | } 39 | 40 | public static String toCSV(List list) { 41 | try { 42 | String json = AppConfig.getObjectMapper().writeValueAsString(list); 43 | JFlatCustom jFlat = new JFlatCustom(json); 44 | StringWriter stringWriter = new StringWriter(); 45 | jFlat 46 | .json2Sheet() 47 | .headerSeparator(".") 48 | .write2csv(stringWriter, ','); 49 | return stringWriter.toString(); 50 | } catch (Exception e) { 51 | log.error("Error converting to csv", e); 52 | return ""; 53 | } 54 | } 55 | 56 | public static Number silentParse(String value) { 57 | try { 58 | return Double.valueOf(value); 59 | } catch (Exception ignored) { 60 | } 61 | return -1; 62 | } 63 | 64 | public static String getDefaultText(Map map) { 65 | if (MapUtils.isEmpty(map)) { 66 | return StringUtils.EMPTY; 67 | } 68 | return map.get(DEFAULT_LOCALE); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.config; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; 4 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 5 | import com.fasterxml.jackson.annotation.PropertyAccessor; 6 | import com.fasterxml.jackson.core.JsonGenerator.Feature; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import com.manson.fo76.domain.config.MongoDbConfig; 9 | import com.mongodb.client.MongoClient; 10 | import com.mongodb.client.MongoClients; 11 | import javax.servlet.MultipartConfigElement; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.context.properties.ConfigurationPropertiesScan; 14 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 15 | import org.springframework.boot.web.servlet.MultipartConfigFactory; 16 | import org.springframework.context.annotation.Bean; 17 | import org.springframework.context.annotation.Configuration; 18 | import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; 19 | import org.springframework.core.io.ClassPathResource; 20 | import org.springframework.util.unit.DataSize; 21 | 22 | @Configuration 23 | @EnableConfigurationProperties 24 | @ConfigurationPropertiesScan 25 | public class AppConfig { 26 | 27 | private static final ObjectMapper objectMapper = new ObjectMapper(); 28 | 29 | static { 30 | objectMapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE); 31 | objectMapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY); 32 | objectMapper.enable(Feature.IGNORE_UNKNOWN); 33 | objectMapper.setSerializationInclusion(Include.NON_EMPTY); 34 | } 35 | 36 | @Autowired 37 | private MongoDbConfig mongoDbConfig; 38 | 39 | @Bean 40 | public static ObjectMapper getObjectMapper() { 41 | return objectMapper; 42 | } 43 | 44 | @Bean 45 | public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() { 46 | PropertySourcesPlaceholderConfigurer propsConfig 47 | = new PropertySourcesPlaceholderConfigurer(); 48 | propsConfig.setLocation(new ClassPathResource("git.properties")); 49 | propsConfig.setIgnoreResourceNotFound(true); 50 | propsConfig.setIgnoreUnresolvablePlaceholders(true); 51 | return propsConfig; 52 | } 53 | 54 | @Bean 55 | public MongoClient mongoClient() { 56 | return MongoClients.create(mongoDbConfig.getFullUrl()); 57 | } 58 | 59 | @Bean 60 | public MultipartConfigElement multipartConfigElement() { 61 | MultipartConfigFactory factory = new MultipartConfigFactory(); 62 | factory.setMaxFileSize(DataSize.ofBytes(512000000L)); 63 | factory.setMaxRequestSize(DataSize.ofBytes(512000000L)); 64 | return factory.createMultipartConfig(); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/ui/component/dialog/ItemStatsDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {InfoDialog} from "./InfoDialog"; 3 | import {Item} from "../../service/domain"; 4 | 5 | export class ItemStatsDialog extends InfoDialog { 6 | 7 | header = 'Item statistics'; 8 | 9 | private static buildStatsObject(items: Array) { 10 | const statsObject = { 11 | filters: {}, 12 | legendaryStats: {}, 13 | }; 14 | // @ts-ignore 15 | for (let item of items) { 16 | // @ts-ignore 17 | let filterStat = statsObject.filters[item.filterFlag]; 18 | // @ts-ignore 19 | statsObject.filters[item.filterFlag] = ItemStatsDialog.buildStatObject(filterStat, item); 20 | 21 | if (item.isLegendary) { 22 | // @ts-ignore 23 | let legendaryStats = statsObject.legendaryStats[item.numLegendaryStars]; 24 | // @ts-ignore 25 | statsObject.legendaryStats[item.numLegendaryStars] = ItemStatsDialog.buildStatObject(legendaryStats, item); 26 | } 27 | } 28 | return statsObject; 29 | } 30 | 31 | private static buildStatObject(statObject: any, item: Item) { 32 | if (!statObject) { 33 | statObject = { 34 | count: 0, 35 | items: 0, 36 | weight: 0, 37 | }; 38 | } 39 | statObject.count += 1; 40 | statObject.items += item.count; 41 | if (item.itemDetails) { 42 | statObject.weight += item.itemDetails.totalWeight; 43 | } 44 | return statObject; 45 | } 46 | 47 | private static renderContent(items: Array) { 48 | const itemStats = ItemStatsDialog.buildStatsObject(items); 49 | const legendaryStats = []; 50 | for (let [key, value] of Object.entries(itemStats.legendaryStats)) { 51 | legendaryStats.push(ItemStatsDialog.renderStatObject(`# of stars ${key}`, value)); 52 | } 53 | const weightStats = []; 54 | for (let [key, value] of Object.entries(itemStats.filters)) { 55 | weightStats.push(ItemStatsDialog.renderStatObject(key, value)); 56 | } 57 | 58 | return ( 59 | 60 |
61 |

Legendary items stats

62 | {legendaryStats} 63 |
64 | 65 |
66 |

Weight stats

67 | {weightStats} 68 |
69 |
70 | ); 71 | 72 | } 73 | 74 | private static renderStatObject(title: string, stat: any) { 75 | return ( 76 | 77 | {title}
78 | Total count(distinct): {stat.count}
79 | Total weight: {stat.weight}
80 | Total items: {stat.items}

81 |
82 | ); 83 | } 84 | 85 | show(items: Array) { 86 | super.show(this.header, ItemStatsDialog.renderContent(items)); 87 | } 88 | } -------------------------------------------------------------------------------- /src/main/ui/component/config/schema.ts: -------------------------------------------------------------------------------- 1 | export const as3KeyCodes: { [key: string]: string } = { 2 | "0": "48", 3 | "1": "49", 4 | "2": "50", 5 | "3": "51", 6 | "4": "52", 7 | "5": "53", 8 | "6": "54", 9 | "7": "55", 10 | "8": "56", 11 | "9": "57", 12 | "Numpad 0": "96", 13 | "Numpad 1": "97", 14 | "Numpad 2": "98", 15 | "Numpad 3": "99", 16 | "Numpad 4": "100", 17 | "Numpad 5": "101", 18 | "Numpad 6": "102", 19 | "Numpad 7": "103", 20 | "Numpad 8": "104", 21 | "Numpad 9": "105", 22 | "Numpad *": "106", 23 | "Numpad +": "107", 24 | "Numpad Enter": "13", 25 | "Numpad -": "109", 26 | "Numpad /": "111", 27 | "A": "65", 28 | "B": "66", 29 | "C": "67", 30 | "D": "68", 31 | "E": "69", 32 | "F": "70", 33 | "G": "71", 34 | "H": "72", 35 | "I": "73", 36 | "J": "74", 37 | "K": "75", 38 | "L": "76", 39 | "M": "77", 40 | "N": "78", 41 | "O": "79", 42 | "P": "80", 43 | "Q": "81", 44 | "R": "82", 45 | "S": "83", 46 | "T": "84", 47 | "U": "85", 48 | "V": "86", 49 | "W": "87", 50 | "X": "88", 51 | "Y": "89", 52 | "Z": "90", 53 | "Backspace": "8", 54 | "Tab": "9", 55 | "Shift": "16", 56 | "Control": "17", 57 | "Caps Lock": "20", 58 | "Esc": "27", 59 | "Spacebar": "32", 60 | "Page Up": "33", 61 | "Page Down": "34", 62 | "End": "35", 63 | "Home": "36", 64 | "Left Arrow": "37", 65 | "Up Arrow": "38", 66 | "Right Arrow": "39", 67 | "Down Arrow": "40", 68 | "Insert": "45", 69 | "Delete": "46", 70 | "Num Lock": "144", 71 | "ScrLk": "145", 72 | "Pause/Break": "19", 73 | ":": "186", 74 | ";": "186", 75 | "=": "187", 76 | "+": "187", 77 | "-": "189", 78 | "_": "189", 79 | "/": "191", 80 | "?": "191", 81 | "~": "192", 82 | "`": "192", 83 | "[": "219", 84 | "{": "219", 85 | "\\": "220", 86 | "|": "220", 87 | "}": "221", 88 | "]": "221", 89 | "\"": "222", 90 | "'": "222", 91 | ",": "188", 92 | ".": "190", 93 | "F1": "112", 94 | "F2": "113", 95 | "F3": "114", 96 | "F4": "115", 97 | "F5": "116", 98 | "F6": "117", 99 | "F7": "118", 100 | "F8": "119", 101 | "F9": "120", 102 | "F11": "122", 103 | "F12": "123", 104 | "F13": "124", 105 | "F14": "125", 106 | "F15": "126" 107 | }; 108 | 109 | export const as3Keys = Object.keys(as3KeyCodes); 110 | 111 | export const getAs3CharCode = (keyCode: string) => { 112 | for (let i = 0; i < as3Keys.length; i++) { 113 | const char = as3Keys[i]; 114 | const charCode = as3KeyCodes[char]; 115 | if (charCode === keyCode) { 116 | return char; 117 | } 118 | } 119 | } 120 | 121 | export const enum LayoutTypes { 122 | HorizontalLayout = "HorizontalLayout", 123 | VerticalLayout = "VerticalLayout" 124 | } 125 | 126 | export const enum Type { 127 | Control = 'Control', 128 | Group = 'Group', 129 | Category = 'Category', 130 | } 131 | -------------------------------------------------------------------------------- /src/test/java/com/DebugTests.java: -------------------------------------------------------------------------------- 1 | package com; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.manson.domain.config.LegendaryModDescriptor; 6 | import com.manson.domain.fed76.mapping.Fed76ApiMappingEntry; 7 | import com.manson.domain.fed76.mapping.MappingResponse; 8 | import com.manson.fo76.config.AppConfig; 9 | import com.manson.fo76.domain.config.Fed76Config; 10 | import com.manson.fo76.service.Fed76Service; 11 | import java.io.File; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | import java.util.Map; 15 | import org.apache.commons.lang3.StringUtils; 16 | import org.junit.jupiter.api.Test; 17 | 18 | @org.junit.jupiter.api.Disabled 19 | public class DebugTests { 20 | 21 | private static final TypeReference> LEG_MOD_TYPE_REF = new TypeReference>() { 22 | }; 23 | 24 | @Test 25 | void updateLegModsConfigWithAbbreviations() throws Exception { 26 | ObjectMapper om = AppConfig.getObjectMapper(); 27 | File file = new File("src/main/resources/legendaryMods.config.json"); 28 | Fed76Config fed76Config = new Fed76Config(); 29 | Fed76Service fed76Service = new Fed76Service(fed76Config, om); 30 | Map mapping = fed76Service.getMapping().getEffects().getById(); 31 | List descriptors = om.readValue(file, LEG_MOD_TYPE_REF); 32 | for (LegendaryModDescriptor descriptor : descriptors) { 33 | List keys = Arrays.asList(descriptor.getGameId(), descriptor.getGameId().toLowerCase(), descriptor.getGameId().toUpperCase()); 34 | for (String key: keys) { 35 | if (mapping.containsKey(key)) { 36 | List abbreviations = mapping.get(key).getQueries(); 37 | String abbreviation = abbreviations.get(0); 38 | descriptor.setAbbreviation(abbreviation); 39 | descriptor.setAdditionalAbbreviations(abbreviations); 40 | break; 41 | } 42 | } 43 | } 44 | om.writeValue(file, descriptors); 45 | } 46 | 47 | @Test 48 | void testLegModsConfig() throws Exception { 49 | ObjectMapper om = AppConfig.getObjectMapper(); 50 | File file = new File("src/main/resources/legendaryMods.config.json"); 51 | // Fed76Service fed76Service = new Fed76Service(om, null); 52 | // Map mapping = fed76Service.getMapping().getEffects().getById(); 53 | List descriptors = om.readValue(file, LEG_MOD_TYPE_REF); 54 | for (LegendaryModDescriptor descriptor : descriptors) { 55 | String itemType = descriptor.getItemType().name().replace("_", "").replace("RANGED", "").replace("MELEE", ""); 56 | if (!StringUtils.containsIgnoreCase(descriptor.getId(), itemType)) { 57 | System.out.println(descriptor.getGameId()); 58 | } 59 | } 60 | // om.writeValue(file, descriptors); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/ui/service/domain.ts: -------------------------------------------------------------------------------- 1 | import {UploadFileFilters} from "./filter.service"; 2 | 3 | export const MIN_MOD_SUPPORTED_VERSION = 0.4; 4 | 5 | export interface UrlConfig { 6 | name: string 7 | url: string 8 | } 9 | 10 | export interface GitConfig { 11 | buildTimestamp: string 12 | gitCommitId: string 13 | } 14 | 15 | export interface AppInfo { 16 | name: string 17 | version: string 18 | sites: Array 19 | tools: Array 20 | discord: string 21 | github: string 22 | commitUrl: string 23 | gitConfig: GitConfig 24 | } 25 | 26 | export interface ModDataRequest { 27 | modData: ModData 28 | filters: UploadFileFilters 29 | } 30 | 31 | export interface ModData { 32 | version: number 33 | characterInventories: { 34 | string: any 35 | } 36 | user: { 37 | user: string, 38 | password: string, 39 | id?: string, 40 | append?: boolean 41 | } 42 | } 43 | 44 | export interface AuthorResponse { 45 | name: string 46 | logo: string 47 | description: string 48 | url: string 49 | } 50 | 51 | export interface ReviewRatingResponse { 52 | bestRating: string 53 | ratingValue: string 54 | worstRating: string 55 | } 56 | 57 | export interface ReviewResponse { 58 | author: AuthorResponse 59 | dateCreated: string 60 | description: string 61 | name: string 62 | reviewRating: ReviewRatingResponse 63 | url: string 64 | } 65 | 66 | export interface PriceCheckResponse { 67 | name?: string; 68 | price?: number; 69 | review?: ReviewResponse; 70 | timestamp?: string; 71 | path?: string; 72 | description?: string; 73 | } 74 | 75 | export interface UnknownFields { 76 | [key: string]: any 77 | } 78 | 79 | export interface VendingData { 80 | unknownFields?: UnknownFields; 81 | price: number; 82 | machineType: number; 83 | vendedOnOtherMachine: boolean; 84 | } 85 | 86 | export interface OwnerInfo { 87 | unknownFields?: UnknownFields; 88 | accountName: string; 89 | characterName: string; 90 | } 91 | 92 | export interface ItemDetails { 93 | unknownFields?: UnknownFields; 94 | itemSource?: string; 95 | filterFlag?: string; 96 | ownerInfo?: OwnerInfo; 97 | totalWeight?: number; 98 | name?: string; 99 | abbreviation?: string; 100 | abbreviationId?: string; 101 | config?: any; 102 | armorConfig?: any; 103 | legendaryModConfig: any; 104 | legendaryMods?: Array; 105 | stats?: Array; 106 | } 107 | 108 | export interface Stat { 109 | unknownFields?: UnknownFields; 110 | itemCardText?: string; 111 | value?: string; 112 | damageType?: string; 113 | } 114 | 115 | export interface ReportItem { 116 | item: Item, 117 | reason: string 118 | } 119 | 120 | export interface Item { 121 | unknownFields?: UnknownFields; 122 | id: string; 123 | text: string; 124 | serverHandleId?: number; 125 | count: number; 126 | itemValue: number; 127 | weight: number; 128 | itemLevel: number; 129 | numLegendaryStars: number; 130 | isTradable: boolean; 131 | isSpoiled: boolean; 132 | isSetItem: boolean; 133 | isQuestItem: boolean; 134 | isLegendary: boolean; 135 | vendingData?: VendingData; 136 | filterFlag: string; 137 | itemDetails?: ItemDetails; 138 | isWeightless: boolean; 139 | scrapAllowed: boolean; 140 | isAutoScrappable: boolean; 141 | canGoIntoScrapStash: boolean; 142 | isLearnedRecipe: boolean; 143 | priceCheckResponse?: PriceCheckResponse; 144 | } 145 | -------------------------------------------------------------------------------- /src/main/ui/service/utils.ts: -------------------------------------------------------------------------------- 1 | import {Item} from "./domain"; 2 | import {toXML} from "jstoxml"; 3 | import {parse} from "fast-xml-parser"; 4 | 5 | const NOTES = 'NOTES'; 6 | const priceCheckFilterFlags: Array = ['WEAPON', 'ARMOR', 'WEAPON_RANGED', 'WEAPON_MELEE', NOTES]; 7 | 8 | export class Utils { 9 | public static readFile(file: File): Promise { 10 | return new Promise((resolve, reject) => { 11 | const fr = new FileReader(); 12 | fr.onerror = reject; 13 | fr.onload = () => { 14 | const data: string | ArrayBuffer | null = fr.result; 15 | try { 16 | resolve(JSON.parse(data)); 17 | } catch (e) { 18 | reject(e); 19 | } 20 | } 21 | fr.readAsText(file); 22 | }); 23 | } 24 | 25 | public static shouldPriceCheck(item: Item): boolean { 26 | if (!item.isTradable) { 27 | return false; 28 | } 29 | if (item.priceCheckResponse && item.priceCheckResponse.price) { 30 | const price = item.priceCheckResponse.price; 31 | if (price === -1 || price > 0) { 32 | return false; 33 | } 34 | } 35 | const filterFlag = item.filterFlag; 36 | if (filterFlag === NOTES) { 37 | return true; 38 | } 39 | return item.isLegendary && priceCheckFilterFlags.includes(filterFlag); 40 | } 41 | 42 | public static downloadString(text: string, fileType: string, fileName: string) { 43 | const blob = new Blob([text], {type: fileType}); 44 | 45 | const a = document.createElement('a'); 46 | a.download = fileName; 47 | a.href = URL.createObjectURL(blob); 48 | a.dataset.downloadurl = [fileType, a.download, a.href].join(':'); 49 | a.style.display = "none"; 50 | document.body.appendChild(a); 51 | a.click(); 52 | document.body.removeChild(a); 53 | setTimeout(function () { 54 | URL.revokeObjectURL(a.href); 55 | }, 1500); 56 | } 57 | 58 | public static uuidv4() { 59 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 60 | const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 61 | return v.toString(16); 62 | }); 63 | } 64 | 65 | public static getPropertyByPath(obj: any, path: string) { 66 | try { 67 | path = path.replace(/\[(\w+)\]/g, '.$1'); 68 | path = path.replace(/^\./, ''); 69 | const a = path.split('.'); 70 | let o = obj; 71 | while (a.length) { 72 | const n = a.shift(); 73 | // @ts-ignore 74 | if (!(n in o)) return; 75 | // @ts-ignore 76 | o = o[n]; 77 | } 78 | return o; 79 | } catch (e) { 80 | return undefined; 81 | } 82 | } 83 | 84 | public static setPropertyByPath(obj: any, path: string, value: any) { 85 | try { 86 | const a = path.split('.'); 87 | let o = obj; 88 | while (a.length - 1) { 89 | const n = a.shift(); 90 | // @ts-ignore 91 | if (!(n in o)) { 92 | // @ts-ignore 93 | o[n] = {}; 94 | } 95 | // @ts-ignore 96 | o = o[n]; 97 | } 98 | o[a[0]] = value; 99 | } catch (e) { 100 | 101 | } 102 | } 103 | 104 | public static toXML(input: any): string { 105 | return toXML(input, { 106 | indent: ' ' 107 | }); 108 | } 109 | 110 | public static fromXML(input: any): any { 111 | return parse(input); 112 | } 113 | } -------------------------------------------------------------------------------- /src/test/java/com/fo76/ReportedItemsTests.java: -------------------------------------------------------------------------------- 1 | package com.fo76; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.manson.domain.fo76.items.enums.FilterFlag; 6 | import com.manson.domain.itemextractor.ItemConfig; 7 | import com.manson.domain.itemextractor.ItemResponse; 8 | import com.manson.domain.itemextractor.LegendaryMod; 9 | import com.manson.fo76.config.AppConfig; 10 | import com.manson.fo76.domain.ReportedItem; 11 | import com.manson.fo76.helper.Utils; 12 | import com.manson.fo76.service.Fed76Service; 13 | import com.manson.fo76.service.GameConfigHolderService; 14 | import com.manson.fo76.service.GameConfigService; 15 | import com.manson.fo76.service.ItemConverterService; 16 | import java.io.File; 17 | import java.util.List; 18 | import java.util.Objects; 19 | import java.util.stream.Collectors; 20 | import org.apache.commons.collections4.CollectionUtils; 21 | import org.junit.jupiter.api.BeforeAll; 22 | import org.junit.jupiter.api.Disabled; 23 | import org.junit.jupiter.api.Test; 24 | 25 | @Disabled 26 | public class ReportedItemsTests { 27 | 28 | private static final ObjectMapper OM = AppConfig.getObjectMapper(); 29 | private static final TypeReference> REPORTED_ITEMS_REF = new TypeReference>() { 30 | }; 31 | 32 | private static final GameConfigHolderService gameConfigHolderService = new GameConfigHolderService(); 33 | private static final GameConfigService gameConfigService = new GameConfigService(gameConfigHolderService); 34 | private static final Fed76Service fed76Service = new Fed76Service(null, OM); 35 | private static final ItemConverterService itemConverterService = new ItemConverterService(gameConfigService, fed76Service); 36 | 37 | @BeforeAll 38 | static void beforeAll() { 39 | gameConfigHolderService.setObjectMapper(OM); 40 | gameConfigHolderService.setWeaponNamesConfigFile("configs/weapons.config.json"); 41 | gameConfigHolderService.setWeaponNamesConfigFile("configs/weapons.config.json"); 42 | gameConfigHolderService.setLegModsConfigFile("configs/legendaryMods.config.json"); 43 | } 44 | 45 | private static List listReportedItems() throws Exception { 46 | File file = new File("src/test/resources/reported.items.json"); 47 | return OM.readValue(file, REPORTED_ITEMS_REF); 48 | } 49 | 50 | 51 | @Test 52 | void testReportedItems() throws Exception { 53 | List reportedItems = listReportedItems(); 54 | for (ReportedItem reportedItem: reportedItems) { 55 | ItemResponse item = reportedItem.getItem(); 56 | if (item.getItemDetails() == null) { 57 | continue; 58 | } 59 | List legendaryMods = item.getItemDetails().getLegendaryModConfig().getLegendaryMods(); 60 | if (CollectionUtils.isEmpty(legendaryMods)) { 61 | continue; 62 | } 63 | List modTexts = legendaryMods.stream().map(LegendaryMod::getValue).filter(Objects::nonNull).collect(Collectors.toList()); 64 | if (CollectionUtils.isNotEmpty(modTexts)) { 65 | System.out.println(modTexts); 66 | List mods = itemConverterService.getLegendaryMods(modTexts, item.getFilterFlag()); 67 | System.out.println(mods); 68 | } 69 | } 70 | } 71 | 72 | @Test 73 | void testItemNames() throws Exception { 74 | List reportedItems = listReportedItems(); 75 | for (ReportedItem reportedItem: reportedItems) { 76 | ItemResponse item = reportedItem.getItem(); 77 | String text = item.getText(); 78 | FilterFlag filterFlag = item.getFilterFlag(); 79 | ItemConfig itemConfig = itemConverterService.findItemConfig(text, filterFlag); 80 | if (Objects.isNull(itemConfig)) { 81 | // System.out.println(text + "\t is null"); 82 | continue; 83 | } 84 | System.out.println(text + "\t" + Utils.getDefaultText(itemConfig.getTexts())); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/ui/component/config/InventOmatic.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./BaseInventOmatic.scss"; 3 | import {materialCells, materialRenderers} from "@jsonforms/material-renderers"; 4 | import {JsonForms} from "@jsonforms/react"; 5 | import {Button} from "@material-ui/core"; 6 | import {createAjv} from "@jsonforms/core"; 7 | import {Utils} from "../../service/utils"; 8 | import {inventOmaticPipboySchema} from "./pipboySchema"; 9 | import {inventOmaticStashSchema} from "./stashSchema"; 10 | import {Dropdown, DropdownChangeParams} from "primereact/dropdown"; 11 | 12 | 13 | const schemaConfigs = [ 14 | inventOmaticPipboySchema, 15 | inventOmaticStashSchema 16 | ]; 17 | 18 | const ajv = createAjv({useDefaults: true}); 19 | 20 | export class InventOmatic extends React.Component { 21 | 22 | state = { 23 | schema: schemaConfigs[0], 24 | data: schemaConfigs[0].defaultConfig 25 | } 26 | 27 | fileReader: FileReader = new FileReader(); 28 | 29 | constructor(props: any, context?: any) { 30 | super(props, context); 31 | this.setData = this.setData.bind(this); 32 | this.onSubmit = this.onSubmit.bind(this); 33 | this.handleFileChosen = this.handleFileChosen.bind(this); 34 | this.handleFileRead = this.handleFileRead.bind(this); 35 | this.handleSchemaChange = this.handleSchemaChange.bind(this); 36 | } 37 | 38 | private setData({errors, data}: { errors: any, data: any }) { 39 | this.setState({errors, data}); 40 | } 41 | 42 | private onSubmit() { 43 | const {data} = this.state; 44 | const jsonString: string = JSON.stringify(data, null, 10); 45 | Utils.downloadString(jsonString, 'text/json', this.state.schema.fileName); 46 | console.log(data); 47 | } 48 | 49 | handleFileRead() { 50 | // @ts-ignore 51 | const content = JSON.parse(this.fileReader.result); 52 | console.log(content); 53 | this.setState({data: content}); 54 | }; 55 | 56 | handleFileChosen(e: any) { 57 | this.fileReader = new FileReader(); 58 | this.fileReader.onloadend = this.handleFileRead; 59 | this.fileReader.readAsText(e.target.files[0]); 60 | } 61 | 62 | handleSchemaChange(e: DropdownChangeParams) { 63 | this.setState({schema: e.value}); 64 | this.setState({data: e.value.defaultConfig}); 65 | } 66 | 67 | render() { 68 | const {data, schema} = this.state; 69 | 70 | return ( 71 |
72 |
73 |
74 | 79 |
80 |
81 | 85 | 86 | 88 |
89 |
90 |
91 | {schema.name} 92 |
93 |
94 | 104 |
105 |
106 | ); 107 | } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /src/main/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef, useState} from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import 'primereact/resources/themes/bootstrap4-dark-blue/theme.css'; 5 | import 'primereact/resources/primereact.min.css'; 6 | import 'primeicons/primeicons.css'; 7 | import './index.scss'; 8 | import PrimeReact from 'primereact/api'; 9 | import {TableComponent} from "./component/table/TableComponent"; 10 | import {NavBar} from "./component/navbar/NavBar"; 11 | import {FileUploader, FileUploaderStatus} from "./component/fileUploader/FileUploader"; 12 | import {InfoDialog} from "./component/dialog/InfoDialog"; 13 | import {MIN_MOD_SUPPORTED_VERSION} from "./service/domain"; 14 | import {Route, Switch, HashRouter} from "react-router-dom"; 15 | import {SettingsPage} from "./component/settings/SettingsPage"; 16 | import {routes} from "./service/Routes"; 17 | import {InventOmatic} from "./component/config/InventOmatic"; 18 | import {HUDEditor} from "./component/config/HUDEditor"; 19 | import {createMuiTheme, CssBaseline, ThemeProvider} from "@material-ui/core"; 20 | 21 | PrimeReact.ripple = true; 22 | 23 | document.documentElement.style.fontSize = '9px'; 24 | 25 | export const theme = createMuiTheme({ 26 | palette: { 27 | type: "dark" 28 | } 29 | }); 30 | 31 | const modalTexts = { 32 | invalidVersion: (version: any) => { 33 | return `You are using unsupported mod version ${version}. Minimum supported version of the mod: ${MIN_MOD_SUPPORTED_VERSION}. Please, update the mod, extract your items and try uploading new file again.`; 34 | }, 35 | emptyResult: () => { 36 | return `No data received. Try using different upload filters or disable them at all. If issue is persistent, please, join discord channel and send your file to the author.` 37 | }, 38 | errorResult: () => { 39 | return `Something went wrong. Please, reach out author of the mod/website and send the file for investigation. You may ask for help in discord.`; 40 | }, 41 | }; 42 | 43 | const App = () => { 44 | const [tableData, setTableData] = useState([]); 45 | const [fileStatus, setFileStatus] = useState(FileUploaderStatus.NONE); 46 | const dialogRef: any = useRef(null); 47 | 48 | const showModal = (input: string) => { 49 | dialogRef.current.show('Ooops', input); 50 | } 51 | 52 | const onDataError = (e: any) => { 53 | console.error(e); 54 | showModal(modalTexts.errorResult()); 55 | setTableData([]) 56 | setFileStatus(FileUploaderStatus.NONE); 57 | }; 58 | const onDataReceived = (e: any) => { 59 | if (e.length < 1) { 60 | showModal(modalTexts.emptyResult()); 61 | setTableData([]) 62 | setFileStatus(FileUploaderStatus.NONE); 63 | return; 64 | } 65 | setFileStatus(FileUploaderStatus.LOADED); 66 | setTableData(e); 67 | }; 68 | const onStatusUpdate = (e: FileUploaderStatus) => { 69 | setFileStatus(e); 70 | } 71 | 72 | const template = (content: any) => { 73 | return ( 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | {content} 90 | 91 | 92 | 93 | 94 | 95 | ) 96 | }; 97 | 98 | let body: any; 99 | 100 | switch (fileStatus) { 101 | case FileUploaderStatus.ERROR: 102 | // todo: show error 103 | break; 104 | case FileUploaderStatus.LOADED: 105 | if (tableData.length > 1) { 106 | body = (); 107 | } 108 | break; 109 | default: 110 | body = (); 112 | break; 113 | } 114 | return template(body); 115 | }; 116 | 117 | ReactDOM.render( 118 | , 119 | document.getElementById("app") 120 | ); -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/web/api/ItemController.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.web.api; 2 | 3 | 4 | import com.manson.domain.fo76.items.enums.FilterFlag; 5 | import com.manson.domain.itemextractor.ItemConfig; 6 | import com.manson.domain.itemextractor.ItemResponse; 7 | import com.manson.domain.itemextractor.ItemsUploadFilters; 8 | import com.manson.domain.itemextractor.ModData; 9 | import com.manson.domain.itemextractor.ModDataRequest; 10 | import com.manson.fo76.domain.ItemContext; 11 | import com.manson.fo76.domain.ReportedItem; 12 | import com.manson.fo76.helper.Utils; 13 | import com.manson.fo76.service.ItemService; 14 | import java.util.List; 15 | import javax.ws.rs.QueryParam; 16 | import javax.ws.rs.core.MediaType; 17 | import lombok.extern.slf4j.Slf4j; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.web.bind.annotation.GetMapping; 20 | import org.springframework.web.bind.annotation.PostMapping; 21 | import org.springframework.web.bind.annotation.RequestBody; 22 | import org.springframework.web.bind.annotation.RequestMapping; 23 | import org.springframework.web.bind.annotation.RestController; 24 | 25 | @Slf4j 26 | @RestController 27 | @RequestMapping("/items") 28 | public class ItemController { 29 | 30 | private final ItemService itemService; 31 | 32 | @Autowired 33 | public ItemController(ItemService itemService) { 34 | this.itemService = itemService; 35 | } 36 | 37 | @PostMapping(value = "/prepareModDataRaw", consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON) 38 | public Object prepareModDataRaw( 39 | @QueryParam(value = "autoPriceCheck") boolean autoPriceCheck, 40 | @QueryParam(value = "fed76Enhance") boolean fed76Enhance, 41 | @QueryParam(value = "shortenResponse") boolean shortenResponse, 42 | @QueryParam(value = "toCSV") boolean toCSV, 43 | @RequestBody ModData modData) { 44 | try { 45 | ModDataRequest request = new ModDataRequest(); 46 | request.setFilters(new ItemsUploadFilters()); 47 | request.setModData(modData); 48 | ItemContext context = new ItemContext(); 49 | context.setPriceCheck(autoPriceCheck); 50 | context.setFed76Enhance(fed76Enhance); 51 | context.setShortenResponse(shortenResponse); 52 | List responses = itemService.prepareModData(request, context); 53 | if (toCSV) { 54 | return Utils.toCSV(responses); 55 | } 56 | return responses; 57 | } catch (Exception e) { 58 | log.error("Error while preparing mod data {}", modData, e); 59 | throw e; 60 | } 61 | } 62 | 63 | @PostMapping(value = "/prepareModData", consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON) 64 | public List prepareModData(@RequestBody ModDataRequest modDataRequest, 65 | @QueryParam("priceCheck") boolean priceCheck, @QueryParam("fed76Enhance") boolean fed76Enhance, 66 | @QueryParam("shortenResponse") boolean shortenResponse) { 67 | ItemContext context = new ItemContext(); 68 | context.setPriceCheck(priceCheck); 69 | context.setFed76Enhance(fed76Enhance); 70 | context.setShortenResponse(shortenResponse); 71 | try { 72 | return itemService.prepareModData(modDataRequest, context); 73 | } catch (Exception e) { 74 | log.error("Error while preparing mod data {}", modDataRequest, e); 75 | throw e; 76 | } 77 | } 78 | 79 | @PostMapping(value = "/report", consumes = MediaType.APPLICATION_JSON, produces = MediaType.APPLICATION_JSON) 80 | public ReportedItem reportItem(@RequestBody ReportedItem item) { 81 | try { 82 | return itemService.reportItem(item); 83 | } catch (Exception e) { 84 | log.error("Error reporting item {}", item, e); 85 | throw e; 86 | } 87 | } 88 | 89 | // @GetMapping(value = "reported", produces = MediaType.APPLICATION_JSON) 90 | public List getAllReportedItems() { 91 | return itemService.getAllReportedItems(); 92 | } 93 | 94 | @GetMapping(value = "/itemConfig", produces = MediaType.APPLICATION_JSON) 95 | public ItemConfig getItemConfig(@QueryParam(value = "name") String name, @QueryParam(value = "filterFlag") String filterFlag) { 96 | return itemService.findItemConfig(name, FilterFlag.fromString(filterFlag)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/ui/service/table.columns.tsx: -------------------------------------------------------------------------------- 1 | import {Item} from "./domain"; 2 | import React from "react"; 3 | 4 | export interface ColumnOptions { 5 | filter: boolean, 6 | sort: boolean 7 | } 8 | 9 | export interface SimpleColumn { 10 | header: string; 11 | field: string; 12 | visible?: boolean; 13 | isRating?: boolean; 14 | isBool?: boolean; 15 | } 16 | 17 | export interface ColumnDefinition extends SimpleColumn { 18 | getValue?: (object: Item) => any; 19 | options?: ColumnOptions 20 | } 21 | 22 | const getObjectValue = (path: string, obj: any) => { 23 | if (!obj) { 24 | return ''; 25 | } 26 | let paths = path.split('.') 27 | , current = obj 28 | , i; 29 | 30 | for (i = 0; i < paths.length; ++i) { 31 | if (current[paths[i]] == undefined) { 32 | return undefined; 33 | } else { 34 | current = current[paths[i]]; 35 | } 36 | } 37 | return current; 38 | }; 39 | 40 | export const createColumnDef = ({header, field, visible = true, isRating = false, isBool = false}: ColumnDefinition): ColumnDefinition => { 41 | return { 42 | header, 43 | field, 44 | visible, 45 | isRating, 46 | isBool, 47 | getValue(obj: any) { 48 | return getObjectValue(field, obj); 49 | }, 50 | options: { 51 | filter: true, 52 | sort: true, 53 | } 54 | } 55 | }; 56 | 57 | const getLegModField = (index: number, field: string) => { 58 | return `itemDetails.legendaryModConfig.legendaryMods.${index}.${field}` 59 | }; 60 | 61 | export const columns = [ 62 | createColumnDef({header: 'Name', field: 'text'}), 63 | createColumnDef({header: 'Name converted', field: 'itemDetails.name', visible: false}), 64 | createColumnDef({header: 'Account', field: 'itemDetails.ownerInfo.accountName'}), 65 | createColumnDef({header: 'Character', field: 'itemDetails.ownerInfo.characterName'}), 66 | createColumnDef({header: 'Stars', field: 'numLegendaryStars', isRating: true}), 67 | createColumnDef({header: 'Abbr', field: 'itemDetails.legendaryModConfig.abbreviation'}), 68 | createColumnDef({header: 'Type', field: 'filterFlag'}), 69 | createColumnDef({ 70 | header: 'Armor Grade', 71 | field: 'itemDetails.armorConfig.armorGrade', 72 | visible: false 73 | }), 74 | createColumnDef({header: 'Level', field: 'itemLevel'}), 75 | createColumnDef({header: 'Count', field: 'count'}), 76 | createColumnDef({header: '1 star', field: getLegModField(0, 'value'), visible: false}), 77 | createColumnDef({header: '2 star', field: getLegModField(1, 'value'), visible: false}), 78 | createColumnDef({header: '3 star', field: getLegModField(2, 'value'), visible: false}), 79 | createColumnDef({header: 'Prefix', field: getLegModField(0, 'text')}), 80 | createColumnDef({header: 'Major', field: getLegModField(1, 'text')}), 81 | createColumnDef({header: 'Minor', field: getLegModField(2, 'text')}), 82 | createColumnDef({header: 'Fed76 Price', field: 'priceCheckResponse.price'}), 83 | createColumnDef({ 84 | header: 'Fed76 Description', 85 | field: 'priceCheckResponse.description', 86 | visible: false 87 | }), 88 | createColumnDef({ 89 | header: 'Fed76 Value', 90 | field: 'priceCheckResponse.reviewDescription', 91 | visible: false 92 | }), 93 | createColumnDef({ 94 | header: 'Fed76 Plan verdict', 95 | field: 'priceCheckResponse.verdict', 96 | visible: false 97 | }), 98 | createColumnDef({ 99 | header: 'Fed76 min price', 100 | field: 'priceCheckResponse.minPrice', 101 | visible: false 102 | }), 103 | createColumnDef({ 104 | header: 'Fed76 max price', 105 | field: 'priceCheckResponse.maxPrice', 106 | visible: false 107 | }), 108 | createColumnDef({header: 'Description', field: 'description', visible: false}), 109 | createColumnDef({header: 'Tradable', field: 'isTradable', visible: false, isBool: true}), 110 | createColumnDef({header: 'Legendary', field: 'isLegendary', visible: false, isBool: true}), 111 | createColumnDef({ 112 | header: 'Learned recipe', 113 | field: 'isLearnedRecipe', 114 | visible: false, 115 | isBool: true 116 | }), 117 | createColumnDef({header: 'Source', field: 'itemDetails.itemSource'}), 118 | createColumnDef({header: 'Weight', field: 'weight', visible: false}), 119 | createColumnDef({header: 'Total weight', field: 'itemDetails.totalWeight', visible: false}), 120 | createColumnDef({header: 'Vendor price', field: 'vendingData.price', visible: false}) 121 | ]; -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/service/PriceRequestBuilder.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.service; 2 | 3 | import com.manson.domain.config.ArmorConfig; 4 | import com.manson.domain.fed76.PriceCheckRequest; 5 | import com.manson.domain.fed76.pricing.LegendaryMod; 6 | import com.manson.domain.fed76.pricing.VendorData; 7 | import com.manson.domain.fo76.items.enums.ArmorGrade; 8 | import com.manson.domain.itemextractor.ItemConfig; 9 | import com.manson.domain.itemextractor.ItemDetails; 10 | import com.manson.domain.itemextractor.ItemResponse; 11 | import com.manson.domain.itemextractor.LegendaryModConfig; 12 | import com.manson.fo76.domain.fed76.PriceEnhanceRequest; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.Objects; 16 | import java.util.stream.Collectors; 17 | import org.apache.commons.collections4.CollectionUtils; 18 | import org.apache.commons.lang3.StringUtils; 19 | 20 | public class PriceRequestBuilder { 21 | 22 | private final ItemResponse itemResponse; 23 | 24 | public PriceRequestBuilder(ItemResponse itemResponse) { 25 | this.itemResponse = itemResponse; 26 | } 27 | 28 | public PriceEnhanceRequest createPriceEnhanceRequest() { 29 | if (itemResponse == null || itemResponse.getItemDetails() == null || itemResponse.getVendingData() == null 30 | || itemResponse.getVendingData().getPrice() == null || itemResponse.getVendingData().getPrice() <= 0) { 31 | return null; 32 | } 33 | ItemDetails itemDetails = itemResponse.getItemDetails(); 34 | PriceEnhanceRequest request = new PriceEnhanceRequest(); 35 | request.setItemName(itemDetails.getName()); 36 | request.setGameId(itemDetails.getConfig().getGameId()); 37 | request.setVendingData( 38 | VendorData 39 | .builder() 40 | .price(itemResponse.getVendingData().getPrice()) 41 | .build() 42 | ); 43 | ArmorConfig config = itemDetails.getArmorConfig(); 44 | if (Objects.nonNull(config) && config.getArmorGrade() != ArmorGrade.Unknown) { 45 | request.setArmorConfig(config); 46 | } 47 | request.setLegendary(itemResponse.getIsLegendary()); 48 | LegendaryModConfig modConfig = itemDetails.getLegendaryModConfig(); 49 | if (CollectionUtils.isNotEmpty(modConfig.getLegendaryMods())) { 50 | List mods = modConfig.getLegendaryMods() 51 | .stream().map(x -> { 52 | com.manson.domain.fed76.pricing.LegendaryMod legendaryMod = 53 | new com.manson.domain.fed76.pricing.LegendaryMod(); 54 | legendaryMod.setAbbreviation(x.getAbbreviation()); 55 | legendaryMod.setGameId(x.getGameId()); 56 | legendaryMod.setText(x.getText()); 57 | return legendaryMod; 58 | }).collect(Collectors.toList()); 59 | request.setLegendaryMods(mods); 60 | } 61 | if (request.isValid()) { 62 | return request; 63 | } 64 | return null; 65 | } 66 | 67 | public PriceCheckRequest createPriceCheckRequest() { 68 | PriceCheckRequest request = new PriceCheckRequest(); 69 | ItemDetails itemDetails = itemResponse.getItemDetails(); 70 | ItemConfig configName = itemDetails.getConfig(); 71 | if (Objects.isNull(configName)) { 72 | return request; 73 | } 74 | request.setFilterFlag(configName.getType()); 75 | List ids = new ArrayList<>(); 76 | ids.add(configName.getGameId()); 77 | LegendaryModConfig modConfig = itemDetails.getLegendaryModConfig(); 78 | if (CollectionUtils.isNotEmpty(modConfig.getLegendaryMods())) { 79 | List modIds = 80 | modConfig.getLegendaryMods().stream().map(com.manson.domain.itemextractor.LegendaryMod::getGameId) 81 | .collect(Collectors.toList()); 82 | ids.addAll(modIds); 83 | request.setMods(String.join("/", modIds)); 84 | } 85 | ArmorConfig armorConfig = itemDetails.getArmorConfig(); 86 | if (armorConfig != null && armorConfig.getArmorGrade() != ArmorGrade.Unknown && StringUtils 87 | .isNotBlank(armorConfig.getGradeId())) { 88 | request.setGrade(armorConfig.getArmorGrade()); 89 | request.setGradeId(armorConfig.getGradeId()); 90 | ids.add(armorConfig.getGradeId()); 91 | } 92 | request.setIds(ids); 93 | request.setItem(configName.getGameId()); 94 | return request; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/com/fo76/jdump/Queries.java: -------------------------------------------------------------------------------- 1 | package com.fo76.jdump; 2 | 3 | public class Queries { 4 | 5 | private static final String SELECT = "SELECT * from fo76jdump WHERE "; 6 | private static final String PLAYABLE_NAME_NOT_NULL = " and flags not like '%Non-Playable%' and name != 'null'"; 7 | public static final String LEG_MODS = SELECT 8 | + "signature = \"OMOD\" and editorid like '%mod_Legendary%'";// + PLAYABLE_NAME_NOT_NULL; 9 | public static final String AMMO = SELECT + "signature = \"AMMO\"" + PLAYABLE_NAME_NOT_NULL; 10 | public static final String ARMO = SELECT + "signature = \"ARMO\"" + PLAYABLE_NAME_NOT_NULL; 11 | public static final String WEAPON = SELECT + "signature = 'WEAP'\n" 12 | + PLAYABLE_NAME_NOT_NULL 13 | + " and editorid not like '%Dummy%'\n" 14 | + " and editorid not like '%Test%'\n" 15 | + " and editorid not like '%Temp%'\n" 16 | + " and editorid not like 'POST%'\n" 17 | + " and editorid not like 'CUT%'\n" 18 | + " and editorid not like 'DEPRECATED%'\n" 19 | + " and editorid not like 'Workshop%'\n" 20 | + " and editorid not like 'zzz%'\n" 21 | + " and editorid not like 'Survival%'\n" 22 | + " and editorid not like 'V96%'\n" 23 | + " and editorid not like '%DUPLICATE%'\n" 24 | + " and editorid not like '%OBSOLETE%'\n" 25 | + " and editorid not like 'DLC05WorkshopFireworkWeapon%'\n" 26 | + " and editorid not like 'DELETED%'\n" 27 | + " and editorid not like 'ATX_KDInkwell_Quill%'\n" 28 | + " and editorid not like 'W05%'\n" 29 | + " and editorid not like 'Debug%'\n" 30 | + " and editorid not like '%Epic'\n" 31 | + " and editorid not like 'ATX_CroquetMallet_Red%'\n" 32 | + " and editorid not like '%Babylon%'\n" 33 | + " and editorid not like 'MTR06%'\n" 34 | + " and editorid not like 'MTR05_10mm_ScorchedTrainer%'\n" 35 | + " and editorid not like 'P01B_Mini_Robot01_Binoculars%'"; 36 | 37 | public static final String ARMOR = SELECT + "signature = 'ARMO'\n" 38 | + PLAYABLE_NAME_NOT_NULL 39 | + " and editorid not like '%Dummy%'\n" 40 | + " and editorid not like '%Test%'\n" 41 | + " and editorid not like '%Temp%'\n" 42 | + " and editorid not like 'POST%'\n" 43 | + " and editorid not like 'CUT%'\n" 44 | + " and editorid not like 'DEPRECATED%'\n" 45 | + " and editorid not like 'Workshop%'\n" 46 | + " and editorid not like 'zzz%'\n" 47 | + " and editorid not like 'Survival%'\n" 48 | + " and editorid not like 'V96%'\n" 49 | + " and editorid not like '%DUPLICATE%'\n" 50 | + " and editorid not like '%OBSOLETE%'\n" 51 | + " and editorid not like 'DLC05WorkshopFireworkWeapon%'\n" 52 | + " and editorid not like 'DELETED%'\n" 53 | + " and editorid not like 'ATX_KDInkwell_Quill%'\n" 54 | + " and editorid not like 'W05%'\n" 55 | + " and editorid not like 'Debug%'\n" 56 | + " and editorid not like '%Epic'\n" 57 | + " and editorid not like 'ATX_CroquetMallet_Red%'\n" 58 | + " and editorid not like '%Babylon%'\n" 59 | + " and editorid not like 'MTR06%'\n" 60 | + " and editorid not like 'MTR05_10mm_ScorchedTrainer%'\n" 61 | + " and editorid not like 'P01B_Mini_Robot01_Binoculars%'"; 62 | 63 | public static final String PLANS_RECIPES = SELECT 64 | + "(name like \"%plan%\" or name like \"%recipe%\")\n" 65 | + " and signature = \"BOOK\"\n" 66 | + " and editorid not like '%Dummy%'\n" 67 | + " and editorid not like '%Test%'\n" 68 | + " and editorid not like '%Temp%'\n" 69 | + " and editorid not like 'POST%'\n" 70 | + " and editorid not like 'CUT%'\n" 71 | + " and editorid not like 'DEPRECATED%'\n" 72 | + " and editorid not like 'Workshop%'\n" 73 | + " and editorid not like 'zzz%'\n" 74 | + " and editorid not like 'Survival%'\n" 75 | + " and editorid not like 'V96%'\n" 76 | + " and editorid not like '%DUPLICATE%'\n" 77 | + " and editorid not like '%OBSOLETE%'\n" 78 | + " and editorid not like 'DLC05WorkshopFireworkWeapon%'\n" 79 | + " and editorid not like 'DELETED%'\n" 80 | + " and editorid not like 'ATX_KDInkwell_Quill%'\n" 81 | + " and editorid not like 'W05%'\n" 82 | + " and editorid not like 'Debug%'\n" 83 | + " and editorid not like '%Epic'\n" 84 | + " and editorid not like 'ATX_CroquetMallet_Red%'\n" 85 | + " and editorid not like '%Babylon%'\n" 86 | + " and editorid not like 'MTR06%'\n" 87 | + " and editorid not like 'MTR05_10mm_ScorchedTrainer%'\n" 88 | + " and editorid not like 'P01B_Mini_Robot01_Binoculars%'"; 89 | 90 | 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/ui/component/navbar/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Menubar} from 'primereact/menubar'; 3 | 4 | import {Button} from "primereact/button"; 5 | import { AppInfo, UrlConfig } from "../../service/domain"; 6 | import './NavBar.scss'; 7 | import {gameApiService} from "../../service/game.api.service"; 8 | import {routes} from "../../service/Routes"; 9 | import {MenuItem} from "primereact/menuitem"; 10 | 11 | export class NavBar extends React.Component { 12 | 13 | private static renderButtons(buttons: Array) { 14 | return () => { 15 | if (!buttons || buttons.length < 1) { 16 | return ''; 17 | } 18 | return buttons.map((el: MenuItem) => { 19 | return ( 20 | 21 | 135 | 137 | 138 | 139 | 140 |
141 |
142 | 144 | 146 |
147 |
148 | 149 | 150 | 151 | 152 | 153 | ); 154 | } 155 | } -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/service/Fed76Service.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.service; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.manson.domain.fed76.PriceCheckRequest; 5 | import com.manson.domain.fed76.PriceEstimate; 6 | import com.manson.domain.fed76.PriceType; 7 | import com.manson.domain.fed76.mapping.MappingResponse; 8 | import com.manson.domain.fed76.response.BasePriceCheckResponse; 9 | import com.manson.domain.fed76.response.ItemPriceCheckResponse; 10 | import com.manson.domain.fed76.response.PlanPriceCheckResponse; 11 | import com.manson.domain.fo76.items.enums.ArmorGrade; 12 | import com.manson.domain.fo76.items.enums.FilterFlag; 13 | import com.manson.fo76.domain.config.Fed76Config; 14 | import com.manson.fo76.domain.fed76.PriceCheckCacheItem; 15 | import com.manson.fo76.domain.fed76.PriceEnhanceRequest; 16 | import com.manson.fo76.repository.PriceCheckRepository; 17 | import java.util.List; 18 | import java.util.Objects; 19 | import javax.ws.rs.client.Entity; 20 | import javax.ws.rs.client.WebTarget; 21 | import javax.ws.rs.core.MediaType; 22 | import javax.ws.rs.core.Response; 23 | import lombok.extern.slf4j.Slf4j; 24 | import org.apache.commons.collections4.CollectionUtils; 25 | import org.apache.commons.lang3.StringUtils; 26 | import org.springframework.beans.factory.annotation.Autowired; 27 | import org.springframework.stereotype.Service; 28 | 29 | @Slf4j 30 | @Service 31 | public class Fed76Service extends BaseRestClient { 32 | 33 | private final Fed76Config fed76Config; 34 | 35 | private PriceCheckRepository priceCheckRepository; 36 | 37 | public Fed76Service(@Autowired Fed76Config fed76Config, @Autowired ObjectMapper objectMapper) { 38 | super(objectMapper); 39 | this.fed76Config = fed76Config; 40 | } 41 | 42 | 43 | @Autowired 44 | public void setPriceCheckRepository(PriceCheckRepository priceCheckRepository) { 45 | this.priceCheckRepository = priceCheckRepository; 46 | } 47 | 48 | private BasePriceCheckResponse performRequest(PriceCheckRequest request) { 49 | if (!request.isValid()) { 50 | return new BasePriceCheckResponse(); 51 | } 52 | PriceType type = PriceType.ITEM; 53 | Class respClass = ItemPriceCheckResponse.class; 54 | WebTarget webTarget; 55 | if (request.getFilterFlag() == FilterFlag.NOTES) { 56 | webTarget = planPriceCheck(request.getItem()); 57 | type = PriceType.PLAN; 58 | respClass = PlanPriceCheckResponse.class; 59 | } else { 60 | if (fed76Config.isUseIdForPriceCheck()) { 61 | webTarget = armorWeaponPriceCheck(request.getIds()); 62 | } else { 63 | webTarget = armorWeaponPriceCheck(request.getItem(), request.getMods(), request.getGrade().getValue()); 64 | } 65 | } 66 | try { 67 | BasePriceCheckResponse response = webTarget.request().accept(MediaType.APPLICATION_JSON_TYPE).get(respClass); 68 | if (Objects.nonNull(response)) { 69 | response.setType(type); 70 | } 71 | return response; 72 | } catch (Exception e) { 73 | log.error("Error requesting price check {}\r\n{}", webTarget.getUri(), request); 74 | } 75 | return new BasePriceCheckResponse(); 76 | } 77 | 78 | public final MappingResponse getMapping() { 79 | WebTarget webResource = this.client.target(fed76Config.getMappingUrl()); 80 | return webResource.request().accept(MediaType.APPLICATION_JSON_TYPE).get(MappingResponse.class); 81 | } 82 | 83 | public final PriceEstimate priceCheck(PriceCheckRequest request) { 84 | BasePriceCheckResponse response; 85 | if (!request.isValid()) { 86 | return null; 87 | } 88 | List requests = priceCheckRepository.findByRequestId(request.toId()); 89 | if (CollectionUtils.isEmpty(requests)) { 90 | response = performRequest(request); 91 | PriceCheckAdapter adapter = new PriceCheckAdapter(response, request.getFilterFlag()); 92 | List errors = adapter.getErrors(); 93 | if (CollectionUtils.isNotEmpty(errors)) { 94 | log.error("Error price check {} / {}", request, response); 95 | return adapter.toPriceEstimate(errors); 96 | } 97 | BasePriceCheckResponse priceCheckResponse = saveToCache(adapter.toCacheItem(request)).getResponse(); 98 | return new PriceCheckAdapter(priceCheckResponse, request.getFilterFlag()).toPriceEstimate(); 99 | } else { 100 | log.debug("Found price check request in cache: {}", request); 101 | for (PriceCheckCacheItem cacheItem : requests) { 102 | PriceCheckAdapter adapter = new PriceCheckAdapter(cacheItem.getResponse(), request.getFilterFlag()); 103 | List errors = adapter.getErrors(); 104 | boolean hasErrors = CollectionUtils.isEmpty(adapter.getErrors()); 105 | if (adapter.isResponseExpired() || hasErrors) { 106 | priceCheckRepository.delete(cacheItem); 107 | return priceCheck(request); 108 | } 109 | if (CollectionUtils.isEmpty(errors)) { 110 | return adapter.toPriceEstimate(); 111 | } 112 | } 113 | } 114 | return null; 115 | } 116 | 117 | private PriceCheckCacheItem saveToCache(PriceCheckCacheItem cacheItem) { 118 | return priceCheckRepository.save(cacheItem); 119 | } 120 | 121 | public Response enhancePriceCheck(PriceEnhanceRequest request) { 122 | return client 123 | .target(fed76Config.getPriceEnhanceUrl()) 124 | .request() 125 | .accept(MediaType.APPLICATION_JSON_TYPE) 126 | .post(Entity.json(request)); 127 | } 128 | 129 | private WebTarget armorWeaponPriceCheck(String item, String mods, String grade) { 130 | WebTarget webTarget = client 131 | .target(fed76Config.getItemPricingUrl()) 132 | .queryParam("item", item) 133 | .queryParam("mods", mods); 134 | if (StringUtils.isNotBlank(grade) && !StringUtils.equalsIgnoreCase(ArmorGrade.Unknown.getValue(), grade)) { 135 | webTarget = webTarget.queryParam("grade", grade); 136 | } 137 | return webTarget; 138 | } 139 | 140 | private WebTarget armorWeaponPriceCheck(List ids) { 141 | WebTarget webTarget = client.target(fed76Config.getItemPricingUrl()); 142 | String idsParam = String.join("-", ids); 143 | webTarget = webTarget.queryParam("ids", idsParam); 144 | return webTarget; 145 | } 146 | 147 | private WebTarget planPriceCheck(String item) { 148 | return client 149 | .target(fed76Config.getPlanPricingUrl()) 150 | .queryParam("id", item); 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/test/java/PopulateLegModsConfig.kt: -------------------------------------------------------------------------------- 1 | import com.fasterxml.jackson.core.type.TypeReference 2 | import com.fasterxml.jackson.databind.ObjectMapper 3 | import com.manson.domain.config.ArmorConfig 4 | import com.manson.domain.config.Fo76String 5 | import com.manson.domain.config.LegendaryModDescriptor 6 | import com.manson.domain.config.XTranslatorConfig 7 | import com.manson.domain.fo76.items.enums.ArmorGrade 8 | import com.fo76.XTranslatorParser 9 | import java.io.File 10 | import java.io.IOException 11 | import java.nio.file.Files 12 | import java.nio.file.Paths 13 | import java.util.ArrayList 14 | import java.util.HashMap 15 | import java.util.stream.Collectors 16 | import org.apache.commons.lang3.StringUtils 17 | import org.junit.jupiter.api.Test 18 | 19 | class PopulateLegModsConfig { 20 | @Test 21 | @Throws(IOException::class) 22 | fun dummy2() { 23 | val input = File("./test_resources/ammo_types_ru.xml") 24 | val output = File("ammo.types.json") 25 | val input1 = File("D:\\workspace\\fo76tradeServer\\src\\main\\resources\\ammo.types.json") 26 | val typeReference: TypeReference> = object : TypeReference>() {} 27 | val xTranslatorConfigs = OM.readValue(input1, typeReference) 28 | val fo76Strings = xTranslatorParser.parse(input) 29 | val configs = fo76Strings.stream().map { obj: Fo76String -> XTranslatorConfig.fromFo76String(obj) } 30 | .collect(Collectors.toList()) 31 | for (config in configs) { 32 | for (initial in xTranslatorConfigs) { 33 | XTranslatorConfig.merge(initial, config!!) 34 | } 35 | } 36 | OM.writeValue(output, xTranslatorConfigs) 37 | } 38 | 39 | fun shouldIgnoreConfig(config: XTranslatorConfig): Boolean { 40 | val values = config.texts.values 41 | for (value in values) { 42 | if (StringUtils.isBlank(value)) { 43 | return true 44 | } 45 | for (ignored in fullyIgnoredKeyWords) { 46 | if (StringUtils.equalsIgnoreCase(value, ignored)) { 47 | return true 48 | } 49 | } 50 | for (ignored in ignoredKeyWords) { 51 | if (StringUtils.containsIgnoreCase(value, ignored)) { 52 | return true 53 | } 54 | } 55 | } 56 | return false 57 | } 58 | 59 | private val ignoredKeyWords = listOf("rifle", "harpoon", "Napalmer", "Headlamp", "Revolver", "Shotgun", "Pistol", "Gun", "Slug Buster", "Plasma", "Cryo", "The Fixer", "Shi", "Double", "Handmade", "*", "Assault", "Flame", "The", "Mole miner") 60 | private val fullyIgnoredKeyWords = listOf("black", "light", "heavy") 61 | 62 | @Test 63 | internal fun nameModifiers() { 64 | val input = File("./test_resources/name_modifiers_en.xml") 65 | val output = File("name.modifiers.json") 66 | val fo76Strings = xTranslatorParser.parse(input) 67 | val configs = fo76Strings.stream().map { obj: Fo76String -> XTranslatorConfig.fromFo76String(obj) } 68 | .collect(Collectors.toList()) 69 | val filtered: ArrayList = ArrayList() 70 | val ignoredIds: ArrayList = ArrayList() 71 | for (config in configs) { 72 | if (!shouldIgnoreConfig(config)) { 73 | filtered.add(config) 74 | } else { 75 | config.sid?.let { ignoredIds.add(it) } 76 | } 77 | } 78 | // put "deep pocketed" BEFORE "pocketed" !!! 79 | filtered.sortByDescending { it.texts.values.first().length } 80 | OM.writeValue(output, filtered) 81 | OM.writeValue(File("ignored.name.modifiers.ids.json"), ignoredIds) 82 | } 83 | 84 | 85 | @Test 86 | @Throws(Exception::class) 87 | fun readArmorDr() { 88 | val input = File("./test_resources/armor_dr.txt") 89 | val strings = Files.readAllLines(Paths.get(input.toURI())) 90 | strings.removeAt(0) 91 | val configs: MutableList = ArrayList() 92 | for (line in strings) { 93 | val config = line.split("\t").toTypedArray() 94 | var col = 0 95 | val armorDrConfig = ArmorConfig() 96 | armorDrConfig.helper = config[col++] 97 | armorDrConfig.dr = config[col++].toInt() 98 | armorDrConfig.er = config[col++].toInt() 99 | armorDrConfig.rr = config[col++].toInt() 100 | armorDrConfig.shortTerm = config[col++] 101 | armorDrConfig.armorType = config[col++] 102 | armorDrConfig.armorPart = config[col++] 103 | armorDrConfig.armorGrade = ArmorGrade.fromString(config[col++]) 104 | col++ // do no use this field 105 | armorDrConfig.armorId = config[col++] 106 | armorDrConfig.gradeId = config[col++] 107 | 108 | configs.add(armorDrConfig) 109 | } 110 | OM.writeValue(File("armor.config.json"), configs) 111 | } 112 | 113 | @Test 114 | @Throws(IOException::class) 115 | fun dummy() { 116 | val baseDir = File("./test_resources") 117 | val input = File("D:\\workspace\\fo76tradeServer\\src\\main\\resources\\legendaryMods.config.json") 118 | val descriptors = OM.readValue(input, TYPE_REFERENCE) 119 | val keysFile = File(baseDir, "leg_mods_en_key.xml") 120 | val valuesFile = File(baseDir, "leg_mods_en_value.xml") 121 | val keys = parseXml(keysFile) 122 | val values = parseXml(valuesFile) 123 | for (descriptor in descriptors) { 124 | if (!keys.containsKey(descriptor.id) || !values.containsKey(descriptor.id)) { 125 | continue 126 | } 127 | val k = keys[descriptor.id] 128 | val v = values[descriptor.id] 129 | descriptor.texts[k!!.lang] = k.source!! 130 | // descriptor.translations.put(v!!.lang, v.source) 131 | descriptor.id = k.edid 132 | descriptor.sid = k.sid 133 | descriptor.rec = k.rec.toString() 134 | } 135 | OM.writeValue(File("legModsconfig2.json"), descriptors) 136 | } 137 | 138 | companion object { 139 | private val OM = ObjectMapper() 140 | private val xTranslatorParser = XTranslatorParser(OM) 141 | private val TYPE_REFERENCE: TypeReference> = object : TypeReference>() {} 142 | private fun parseXml(file: File): Map { 143 | val strings: List = xTranslatorParser.parse(file) 144 | return genericListToFo76List(strings) 145 | } 146 | 147 | fun genericListToFo76List(fo76Strings: List): Map { 148 | var map: Map = HashMap() 149 | try { 150 | map = fo76Strings.stream().collect(Collectors.toMap(Fo76String::getEdid, { fo76String: Fo76String -> fo76String }) { _: Fo76String, b: Fo76String -> b }) 151 | } catch (e: Exception) { 152 | e.printStackTrace() 153 | } 154 | return map 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/service/PriceCheckAdapter.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.service; 2 | 3 | import static com.manson.fo76.helper.Utils.silentParse; 4 | 5 | import com.google.common.collect.ImmutableMap; 6 | import com.manson.domain.fed76.PriceCheckRequest; 7 | import com.manson.domain.fed76.PriceEstimate; 8 | import com.manson.domain.fed76.PriceType; 9 | import com.manson.domain.fed76.response.BasePriceCheckResponse; 10 | import com.manson.domain.fed76.response.ItemPriceCheckResponse; 11 | import com.manson.domain.fed76.response.ItemReview; 12 | import com.manson.domain.fed76.response.PlanPriceCheckResponse; 13 | import com.manson.domain.fed76.response.PlanReview; 14 | import com.manson.domain.fed76.response.PlanSubReview; 15 | import com.manson.domain.fed76.response.PriceDetails; 16 | import com.manson.domain.fo76.items.enums.FilterFlag; 17 | import com.manson.fo76.domain.fed76.PriceCheckCacheItem; 18 | import java.time.LocalDateTime; 19 | import java.util.ArrayList; 20 | import java.util.Arrays; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.Objects; 24 | import lombok.extern.slf4j.Slf4j; 25 | import org.apache.commons.lang3.StringUtils; 26 | 27 | @Slf4j 28 | public class PriceCheckAdapter { 29 | 30 | private static final List ERROR_RESPONSES = Arrays.asList( 31 | "Error\n", 32 | "Input Error", 33 | "Nothing matches your query", 34 | "Failed to interpret the key", 35 | "Impossible legendary effects configuration" 36 | ); 37 | private static final Map MAP = ImmutableMap.builder() 38 | .put(FilterFlag.WEAPON, PriceType.ITEM) 39 | .put(FilterFlag.WEAPON_MELEE, PriceType.ITEM) 40 | .put(FilterFlag.WEAPON_RANGED, PriceType.ITEM) 41 | .put(FilterFlag.ARMOR, PriceType.ITEM) 42 | .put(FilterFlag.NOTES, PriceType.PLAN) 43 | .build(); 44 | 45 | private final BasePriceCheckResponse response; 46 | private final FilterFlag filterFlag; 47 | 48 | public PriceCheckAdapter(BasePriceCheckResponse response, FilterFlag filterFlag) { 49 | this.response = response; 50 | this.filterFlag = filterFlag; 51 | } 52 | 53 | public PriceCheckCacheItem toCacheItem(PriceCheckRequest request) { 54 | PriceCheckCacheItem cacheItem = new PriceCheckCacheItem(); 55 | cacheItem.setRequestId(request.toId()); 56 | cacheItem.setResponse(response); 57 | return cacheItem; 58 | } 59 | 60 | public PriceEstimate toPriceEstimate() { 61 | return toPriceEstimate(getErrors()); 62 | } 63 | 64 | public PriceEstimate toPriceEstimate(List errors) { 65 | PriceEstimate priceEstimate = new PriceEstimate(); 66 | priceEstimate.setPath(response.getPath()); 67 | priceEstimate.setName(response.getName()); 68 | priceEstimate.setTimestamp(response.getTimestamp()); 69 | priceEstimate.setPrice(response.getPrice()); 70 | String errorMessage = String.join(", ", errors); 71 | PriceType priceType = MAP.get(filterFlag); 72 | if (PriceType.ITEM == priceType) { 73 | ItemPriceCheckResponse itemResponse = null; 74 | try { 75 | itemResponse = (ItemPriceCheckResponse) response; 76 | } catch (ClassCastException ignored) { 77 | 78 | } 79 | if (Objects.isNull(itemResponse)) { 80 | return priceEstimate; 81 | } 82 | priceEstimate.setDescription(itemResponse.getDescription()); 83 | ItemReview review = itemResponse.getReview(); 84 | if (Objects.isNull(review)) { 85 | return priceEstimate; 86 | } 87 | priceEstimate.setReviewDescription(review.getDescription()); 88 | PriceDetails details = review.getDetails(); 89 | if (Objects.isNull(details)) { 90 | return priceEstimate; 91 | } 92 | priceEstimate.setVendor(details.getVendor()); 93 | priceEstimate.setMinPrice(silentParse(details.getMarketLow()).intValue()); 94 | priceEstimate.setMaxPrice(silentParse(details.getMarketHigh()).intValue()); 95 | priceEstimate.setNiche(details.getNiche()); 96 | priceEstimate.setOriginal(silentParse(details.getOriginal()).intValue()); 97 | } else if (PriceType.PLAN == priceType) { 98 | PlanPriceCheckResponse planResponse = null; 99 | try { 100 | planResponse = (PlanPriceCheckResponse) response; 101 | } catch (ClassCastException ignored) { 102 | 103 | } 104 | if (Objects.isNull(planResponse)) { 105 | return priceEstimate; 106 | } 107 | priceEstimate.setDescription(planResponse.getVerdict()); 108 | PlanReview review = planResponse.getReview(); 109 | if (Objects.isNull(review) || Objects.isNull(review.getReview())) { 110 | return priceEstimate; 111 | } 112 | PlanSubReview subReview = review.getReview(); 113 | priceEstimate.setMinPrice(subReview.getVendorMin()); 114 | priceEstimate.setMaxPrice(subReview.getVendorMax()); 115 | } 116 | 117 | if (StringUtils.isNotBlank(errorMessage)) { 118 | priceEstimate.setDescription(errorMessage); 119 | priceEstimate.setPrice(-1); 120 | priceEstimate.setMaxPrice(-1); 121 | priceEstimate.setMinPrice(-1); 122 | priceEstimate.setOriginal(-1); 123 | } 124 | return priceEstimate; 125 | } 126 | 127 | public List getErrors() { 128 | List errors = new ArrayList<>(); 129 | List responseErrors = new ArrayList<>(); 130 | responseErrors.add(response.getName()); 131 | PriceType type = response.getType(); 132 | if (PriceType.ITEM == type) { 133 | ItemPriceCheckResponse itemResponse = (ItemPriceCheckResponse) response; 134 | responseErrors.add(itemResponse.getDescription()); 135 | ItemReview review = itemResponse.getReview(); 136 | if (Objects.nonNull(review)) { 137 | responseErrors.add(review.getDescription()); 138 | responseErrors.add(review.getName()); 139 | } 140 | } else if (PriceType.PLAN == type) { 141 | PlanPriceCheckResponse planResponse = (PlanPriceCheckResponse) response; 142 | responseErrors.add(planResponse.getMessage()); 143 | } 144 | 145 | for (String error : ERROR_RESPONSES) { 146 | for (String respError : responseErrors) { 147 | if (StringUtils.containsIgnoreCase(respError, error)) { 148 | errors.add(respError); 149 | } 150 | } 151 | } 152 | return errors; 153 | } 154 | 155 | 156 | public boolean isResponseExpired() { 157 | try { 158 | LocalDateTime respDate = LocalDateTime.parse(response.getTimestamp().replace(" ", "T")).plusHours(24); 159 | return respDate.isBefore(LocalDateTime.now()); 160 | } catch (Exception e) { 161 | log.error("Error parsing date {}", response); 162 | } 163 | return true; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/com/manson/fo76/service/GameConfigService.java: -------------------------------------------------------------------------------- 1 | package com.manson.fo76.service; 2 | 3 | import com.google.common.collect.Sets; 4 | import com.manson.domain.config.ArmorConfig; 5 | import com.manson.domain.config.LegendaryModDescriptor; 6 | import com.manson.domain.fo76.ItemCardEntry; 7 | import com.manson.domain.fo76.ItemDescriptor; 8 | import com.manson.domain.fo76.items.enums.DamageType; 9 | import com.manson.domain.fo76.items.enums.FilterFlag; 10 | import com.manson.domain.fo76.items.enums.ItemCardText; 11 | import com.manson.domain.itemextractor.ItemConfig; 12 | import com.manson.domain.itemextractor.Stats; 13 | import java.util.Collection; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.Objects; 17 | import java.util.Set; 18 | import java.util.stream.Collectors; 19 | import lombok.Data; 20 | import lombok.extern.slf4j.Slf4j; 21 | import org.apache.commons.lang3.StringUtils; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.beans.factory.annotation.Value; 24 | import org.springframework.stereotype.Service; 25 | 26 | @Service 27 | @Slf4j 28 | @Data 29 | public class GameConfigService { 30 | 31 | @Value("${game.config.populate.details.all}") 32 | private boolean populateConfigForEverything = true; 33 | 34 | private static final String DOT = "."; 35 | private static final Set SUPPORTED_TYPES_ARMOR = Sets 36 | .newHashSet(FilterFlag.ARMOR, FilterFlag.APPAREL, FilterFlag.POWER_ARMOR); 37 | private final GameConfigHolderService config; 38 | 39 | @Autowired 40 | public GameConfigService(GameConfigHolderService config) { 41 | this.config = config; 42 | } 43 | 44 | private static boolean isSameFilterFlag(FilterFlag first, FilterFlag second) { 45 | boolean same = first == second; 46 | boolean subtype = first.getSubtypes().contains(second) || second.getSubtypes().contains(first); 47 | return same || subtype; 48 | } 49 | 50 | private static String replace(String input) { 51 | if (StringUtils.isBlank(input)) { 52 | return input; 53 | } 54 | return input.replace(DOT, StringUtils.EMPTY).trim(); 55 | } 56 | 57 | public final ItemCardText findItemCardText(ItemCardEntry cardEntry) { 58 | return this.findItemCardText(cardEntry.getText()); 59 | } 60 | 61 | public final ItemCardText findItemCardText(String input) { 62 | if (StringUtils.isBlank(input)) { 63 | return ItemCardText.UNKNOWN; 64 | } 65 | for (ItemCardText itemCardText : ItemCardText.values()) { 66 | for (String value : itemCardText.getValues()) { 67 | if (StringUtils.equalsIgnoreCase(value, input)) { 68 | return itemCardText; 69 | } 70 | } 71 | } 72 | if (config.getAmmoTypes().stream().map(ammoType -> ammoType.getTexts().values()).flatMap(Collection::stream) 73 | .anyMatch(value -> StringUtils.equalsIgnoreCase(value, input))) { 74 | return ItemCardText.AMMO; 75 | } 76 | 77 | return ItemCardText.UNKNOWN; 78 | } 79 | 80 | public final LegendaryModDescriptor findLegendaryModDescriptor(String input, FilterFlag filterFlag) { 81 | if (StringUtils.isBlank(input)) { 82 | return null; 83 | } 84 | return config.getLegModsConfig() 85 | .stream() 86 | .filter(x -> x.isEnabled() && isSameFilterFlag(x.getItemType(), filterFlag)) 87 | .filter(x -> x.isTheSameMod(input, filterFlag)) 88 | .findFirst() 89 | .orElse(null); 90 | } 91 | 92 | public ItemConfig findArmorConfig(String name, FilterFlag filterFlag) { 93 | if (!SUPPORTED_TYPES_ARMOR.contains(filterFlag)) { 94 | return null; 95 | } 96 | String itemName = replace(name); 97 | for (ItemConfig config : config.getArmorNames()) { 98 | for (String text : config.getTexts().values()) { 99 | String cleanItemText = replace(text); 100 | if (StringUtils.containsIgnoreCase(itemName, cleanItemText)) { 101 | return config; 102 | } 103 | } 104 | } 105 | return null; 106 | } 107 | 108 | public ItemConfig findArmorConfig(ItemDescriptor item, FilterFlag filterFlag) { 109 | if (!populateConfigForEverything) { 110 | if (!item.isLegendary() || !item.isTradable()) { 111 | return null; 112 | } 113 | } 114 | return findArmorConfig(item.getText(), filterFlag); 115 | } 116 | 117 | public ItemConfig findPlanConfig(String itemName, FilterFlag filterFlag) { 118 | if (filterFlag != FilterFlag.NOTES) { 119 | return null; 120 | } 121 | for (ItemConfig config : config.getPlanNames()) { 122 | for (String text : config.getTexts().values()) { 123 | if (StringUtils.equalsIgnoreCase(itemName, text)) { 124 | return config; 125 | } 126 | } 127 | } 128 | return null; 129 | } 130 | 131 | public ItemConfig findPlanConfig(ItemDescriptor item, FilterFlag filterFlag) { 132 | return findPlanConfig(item.getText(), filterFlag); 133 | } 134 | 135 | public ItemConfig findWeaponConfig(String itemText, FilterFlag filterFlag) { 136 | String itemName = replace(itemText); 137 | List itemConfigs = config.getWeaponNames().stream() 138 | .filter(x -> isSameFilterFlag(x.getType(), filterFlag)) 139 | .collect(Collectors.toList()); 140 | for (ItemConfig config : itemConfigs) { 141 | for (String text : config.getTexts().values()) { 142 | String cleanItemText = replace(text); 143 | if (StringUtils.containsIgnoreCase(itemName, cleanItemText)) { 144 | return config; 145 | } 146 | } 147 | } 148 | return getSpecialCaseValue(itemName, config.getWeaponNames()); 149 | } 150 | 151 | public ItemConfig getSpecialCaseValue(String itemName, List itemConfigs) { 152 | for (Map.Entry entry : config.getSpecialCasesConfig().entrySet()) { 153 | String cleanItemText = replace(entry.getKey()); 154 | if (StringUtils.containsIgnoreCase(itemName, cleanItemText) || StringUtils 155 | .containsIgnoreCase(cleanItemText, itemName)) { 156 | return itemConfigs.stream() 157 | .filter(x -> StringUtils.equalsIgnoreCase(x.getGameId(), entry.getValue())).findFirst().orElse(null); 158 | } 159 | } 160 | return null; 161 | } 162 | 163 | public ItemConfig findWeaponConfig(ItemDescriptor item, FilterFlag filterFlag) { 164 | if (!populateConfigForEverything) { 165 | if (!item.isLegendary() || !item.isTradable()) { 166 | return null; 167 | } 168 | } 169 | return findWeaponConfig(item.getText(), filterFlag); 170 | } 171 | 172 | private int findDamageTypeValue(List stats, DamageType dmgType) { 173 | for (Stats stat : stats) { 174 | if (stat.getDamageType() == dmgType) { 175 | if (StringUtils.isNotBlank(stat.getValue())) { 176 | return Integer.parseInt(stat.getValue()); 177 | } 178 | } 179 | } 180 | return 0; 181 | } 182 | 183 | public ArmorConfig findArmorType(int dr, int rr, int er) { 184 | for (ArmorConfig conf : config.getArmorConfigs()) { 185 | if (conf.getDr() == dr && conf.getEr() == er && conf.getRr() == rr) { 186 | return conf; 187 | } 188 | } 189 | return null; 190 | } 191 | 192 | public ArmorConfig findArmorType(List stats, String abbreviation) { 193 | int dr = findDamageTypeValue(stats, DamageType.BALLISTIC); 194 | int er = findDamageTypeValue(stats, DamageType.ENERGY); 195 | int rr = findDamageTypeValue(stats, DamageType.RADIATION); 196 | if (StringUtils.containsIgnoreCase(abbreviation, "25R")) { 197 | rr -= 25; 198 | } 199 | 200 | return this.findArmorType(dr, rr, er); 201 | } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /src/test/java/com/fo76/jdump/ConfigUpdaterTest.java: -------------------------------------------------------------------------------- 1 | package com.fo76.jdump; 2 | 3 | import ch.qos.logback.classic.LoggerContext; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.manson.domain.config.LegendaryModDescriptor; 7 | import com.manson.domain.fed76.mapping.Fed76ApiMappingEntry; 8 | import com.manson.domain.fed76.mapping.MappingResponse; 9 | import com.manson.domain.fo76.items.enums.FilterFlag; 10 | import com.manson.fo76.config.AppConfig; 11 | import com.manson.fo76.domain.config.Fed76Config; 12 | import com.manson.fo76.service.Fed76Service; 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.util.ArrayList; 16 | import java.util.Arrays; 17 | import java.util.Comparator; 18 | import java.util.HashSet; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.Set; 22 | import java.util.stream.Collectors; 23 | import org.apache.commons.collections4.CollectionUtils; 24 | import org.apache.commons.lang3.StringUtils; 25 | import org.hibernate.Session; 26 | import org.hibernate.SessionFactory; 27 | import org.hibernate.Transaction; 28 | import org.hibernate.cfg.Configuration; 29 | import org.junit.jupiter.api.BeforeAll; 30 | import org.junit.jupiter.api.Disabled; 31 | import org.junit.jupiter.api.Test; 32 | import org.slf4j.LoggerFactory; 33 | 34 | @Disabled 35 | public class ConfigUpdaterTest { 36 | 37 | private static final String NULL = "null"; 38 | private static final String EMPTY_OBJECT = "{}"; 39 | 40 | private static final ObjectMapper OM = AppConfig.getObjectMapper(); 41 | private static final TypeReference> STRING_MAP_REF = new TypeReference>() { 42 | }; 43 | private static final TypeReference> LEG_MOD_REF = new TypeReference>() { 44 | }; 45 | private static SessionFactory sessionFactory; 46 | private static Fed76Service fed76Service; 47 | 48 | private static final String LEG_MODS_FILE = "src/main/resources/configs/legendaryMods.config.json"; 49 | 50 | @BeforeAll 51 | public static void beforeAll() { 52 | // OM.setSerializationInclusion(Include.ALWAYS); 53 | // disable logging 54 | LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); 55 | loggerContext.stop(); 56 | Configuration configuration = new Configuration(); 57 | configuration.configure(); 58 | sessionFactory = configuration.buildSessionFactory(); 59 | Fed76Config fed76Config = new Fed76Config(); 60 | fed76Config.setMappingUrl("https://fed76.info/pricing/mapping"); 61 | fed76Service = new Fed76Service(fed76Config, OM); 62 | } 63 | 64 | private static List getData(String query) { 65 | Session session = null; 66 | Transaction tx = null; 67 | try { 68 | session = sessionFactory.openSession(); 69 | tx = session.beginTransaction(); 70 | //noinspection unchecked 71 | return session.createNativeQuery(query).addEntity(SqlLiteEntity.class).list(); 72 | } catch (Exception e) { 73 | e.printStackTrace(); 74 | } finally { 75 | if (session != null) { 76 | session.flush(); 77 | } 78 | if (tx != null) { 79 | tx.commit(); 80 | } 81 | } 82 | return new ArrayList<>(); 83 | } 84 | 85 | private static boolean isEmpty(String input) { 86 | boolean isBlank = StringUtils.isBlank(input); 87 | boolean isNull = StringUtils.equalsIgnoreCase(input, NULL); 88 | boolean isEmptyObject = StringUtils.equalsIgnoreCase(input, EMPTY_OBJECT); 89 | return isBlank || isNull || isEmptyObject; 90 | } 91 | 92 | private static Integer parseStarMod(String input) { 93 | try { 94 | String num = input.replaceAll("[^0-9]", ""); 95 | if (StringUtils.isBlank(num) || StringUtils.length(num) > 1) { 96 | return 999; 97 | } 98 | return Integer.parseInt(num); 99 | } catch (Exception e) { 100 | System.out.println("Error parsing star from " + input); 101 | } 102 | return null; 103 | } 104 | 105 | @Test 106 | void updateLegModsConfig() throws Exception { 107 | List data = getData(Queries.LEG_MODS); 108 | MappingResponse mapping = fed76Service.getMapping(); 109 | List descriptors = new ArrayList<>(); 110 | for (SqlLiteEntity entity : data) { 111 | if (isEmpty(entity.getName()) || isEmpty(entity.getName()) || isEmpty(entity.getDesc()) || isEmpty( 112 | entity.getEditorid()) || isEmpty(entity.getFormid())) { 113 | continue; 114 | } 115 | 116 | String editorId = entity.getEditorid(); 117 | LegendaryModDescriptor modDescriptor = new LegendaryModDescriptor(); 118 | modDescriptor.setId(editorId); 119 | modDescriptor.setGameId(entity.getFormid()); 120 | modDescriptor.setTexts(OM.readValue(entity.getName(), STRING_MAP_REF)); 121 | modDescriptor.setTranslations(OM.readValue(entity.getDesc(), STRING_MAP_REF)); 122 | Integer star = parseStarMod(editorId); 123 | if (star == null) { 124 | continue; 125 | } 126 | modDescriptor.setStar(star); 127 | if (StringUtils.containsIgnoreCase(editorId, "weapon")) { 128 | modDescriptor.setItemType(FilterFlag.WEAPON); 129 | } else if (StringUtils.containsIgnoreCase(editorId, "armor")) { 130 | modDescriptor.setItemType(FilterFlag.ARMOR); 131 | } else { 132 | System.out.println("Unknown item type: " + editorId); 133 | continue; 134 | } 135 | List abbreviations = getAbbreviations(mapping, entity.getFormid()); 136 | boolean hasAbbreviations = CollectionUtils.isNotEmpty(abbreviations); 137 | modDescriptor.setEnabled(hasAbbreviations); 138 | if (hasAbbreviations) { 139 | modDescriptor.setAdditionalAbbreviations(abbreviations); 140 | modDescriptor.setAbbreviation(abbreviations.get(0)); 141 | } 142 | if (hasAbbreviations && (star > 3)) { 143 | System.out.println("Need for manual check: " + editorId); 144 | } 145 | 146 | descriptors.add(modDescriptor); 147 | } 148 | descriptors.sort(Comparator.comparingInt(LegendaryModDescriptor::getStar)); 149 | OM.writerWithDefaultPrettyPrinter() 150 | .writeValue(new File("src/main/resources/legendaryMods.config1.json"), descriptors); 151 | } 152 | 153 | private Fed76ApiMappingEntry getFedEntry(Map map, String key) { 154 | List keys = Arrays.asList(key, key.toLowerCase(), key.toUpperCase()); 155 | return keys.stream().filter(map::containsKey).findFirst().map(map::get).orElse(null); 156 | } 157 | 158 | private List getAbbreviations(MappingResponse response, String formId) { 159 | Map byId = response.getEffects().getById(); 160 | Fed76ApiMappingEntry fed76ApiMappingEntry = getFedEntry(byId, formId); 161 | if (fed76ApiMappingEntry != null) { 162 | return fed76ApiMappingEntry.getQueries(); 163 | } 164 | return null; 165 | } 166 | 167 | @Test 168 | public void printMissingLegModIdsAndAddAbbreviation() throws IOException { 169 | MappingResponse mapping = fed76Service.getMapping(); 170 | List mods = OM.readValue(new File(LEG_MODS_FILE), LEG_MOD_REF); 171 | Set strings = mapping.getEffects().getById().keySet(); 172 | for (String id : strings) { 173 | if (StringUtils.isBlank(id) || StringUtils.equalsIgnoreCase(id, "None")) { 174 | continue; 175 | } 176 | if (mods.stream().noneMatch(x -> StringUtils.equalsIgnoreCase(id, x.getGameId()))) { 177 | System.out.println(id); 178 | continue; 179 | } 180 | mods.stream().filter(x -> StringUtils.equalsIgnoreCase(id, x.getGameId())).findFirst().map(x -> { 181 | if (StringUtils.isBlank(x.getAbbreviation())) { 182 | x.setAbbreviation(mapping.getEffects().getById().get(id).getQueries().get(0)); 183 | } 184 | List abbreviations = x.getAdditionalAbbreviations(); 185 | if (CollectionUtils.isEmpty(abbreviations)) { 186 | abbreviations = new ArrayList<>(); 187 | } 188 | Set newAbbr = new HashSet<>(abbreviations); 189 | newAbbr.addAll(mapping.getEffects().getById().get(id).getQueries()); 190 | x.setAdditionalAbbreviations(new ArrayList<>(newAbbr)); 191 | return x; 192 | }); 193 | } 194 | OM.writeValue(new File(LEG_MODS_FILE), mods); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/main/ui/component/config/HUDEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Utils} from "../../service/utils"; 3 | import {Button, Tooltip} from "@material-ui/core"; 4 | import "./HUDEditor.scss"; 5 | 6 | import HelpIcon from '@material-ui/icons/Help'; 7 | import {gameApiService} from "../../service/game.api.service"; 8 | import { 9 | CheckboxComponent, 10 | ColorPickerComponent, 11 | InputNumberComponent 12 | } from "./schema-form/Components"; 13 | 14 | interface HUDElement { 15 | [name: string]: HudConfigElement 16 | } 17 | 18 | interface HudConfigElement { 19 | label: string 20 | id: string 21 | description: string 22 | additionalElements?: Array 23 | additionalConfigs?: Array 24 | fields: Array 25 | } 26 | 27 | export interface HUDField { 28 | label: string 29 | id: string 30 | description?: string 31 | defaultValue: any 32 | min?: number 33 | max?: number 34 | step?: number 35 | type: "NUMERIC" | "COLOR" | "BOOLEAN" 36 | } 37 | 38 | export interface HUDEditorSchema { 39 | [name: string]: HUDElement 40 | } 41 | 42 | 43 | export class HUDEditor extends React.Component { 44 | htmlElements: Array = []; 45 | HUDEditor: any = {}; 46 | fileReader: FileReader = new FileReader(); 47 | data: any = {}; 48 | schema: HUDEditorSchema = {}; 49 | state: any = { 50 | HUDEditor: {}, 51 | htmlElements: [] 52 | }; 53 | 54 | setConfigState(path: string, value: any) { 55 | const configState = JSON.parse(JSON.stringify(this.HUDEditor)); 56 | Utils.setPropertyByPath(this.HUDEditor, path, value); 57 | Utils.setPropertyByPath(configState, path, value); 58 | const prevState = this.state.HUDEditor; 59 | const newState = { 60 | ...prevState, 61 | ...configState 62 | }; 63 | this.setState({ 64 | HUDEditor: { 65 | ...this.state.HUDEditor, 66 | ...newState 67 | } 68 | }); 69 | } 70 | 71 | constructor(props: any, context: any) { 72 | super(props, context); 73 | this.onSubmit = this.onSubmit.bind(this); 74 | this.handleFileChosen = this.handleFileChosen.bind(this); 75 | this.handleFileRead = this.handleFileRead.bind(this); 76 | } 77 | 78 | processHudElement(hudElement: HUDElement, elements: Array, path: string) { 79 | const schemaElKeys = Object.keys(hudElement); 80 | for (const schemaElKey of schemaElKeys) { 81 | const newPath = path + '.' + schemaElKey; 82 | const configEl: HudConfigElement = hudElement[schemaElKey]; 83 | this.processHudConfigElement(configEl, elements, newPath); 84 | } 85 | } 86 | 87 | getFieldValue(hudField: HUDField, path: string): any { 88 | const providedValue = Utils.getPropertyByPath(this.data.HUDEditor, path); 89 | const hudVal = hudField.defaultValue; 90 | if (providedValue !== undefined) { 91 | return providedValue; 92 | } 93 | return hudVal; 94 | } 95 | 96 | processHudConfigElement(configEl: HudConfigElement, elements: Array, path: string) { 97 | this.setConfigState(path, {}); 98 | if (configEl.fields && configEl.fields.length > 0) { 99 | const fieldsElements = configEl.fields.map((hudField) => { 100 | const newPath = path + '.' + hudField.id; 101 | this.setConfigState(newPath, this.getFieldValue(hudField, path)); 102 | return this.createHtmlElement(hudField, newPath); 103 | }); 104 | elements.push( 105 |
106 | 107 | 110 | 111 | {fieldsElements} 112 |
113 | ); 114 | } 115 | if (configEl.additionalConfigs && configEl.additionalConfigs.length > 0) { 116 | const els: Array = []; 117 | configEl.additionalConfigs.forEach(hudConfigEl => { 118 | const newPath = path + '.' + hudConfigEl.id; 119 | this.processHudConfigElement(hudConfigEl, els, newPath); 120 | }); 121 | elements.push(...els); 122 | } 123 | if (configEl.additionalElements && configEl.additionalElements.length > 0) { 124 | const els: Array = []; 125 | configEl.additionalElements.forEach(hudEl => { 126 | this.processHudElement(hudEl, els, path); 127 | }); 128 | elements.push(...els); 129 | } 130 | } 131 | 132 | static downloadConfig(data: any) { 133 | const finalObject = { 134 | HUDEditor: data 135 | }; 136 | const jsonString: string = Utils.toXML(finalObject); 137 | Utils.downloadString(jsonString, 'text/xml', 'HUDEditor.xml'); 138 | } 139 | 140 | onSubmit() { 141 | HUDEditor.downloadConfig(this.state.HUDEditor); 142 | } 143 | 144 | createHtmlElement(hudField: HUDField, path: string) { 145 | const onDataChange = (data: any) => { 146 | this.setConfigState(path, data); 147 | } 148 | switch (hudField.type) { 149 | case "COLOR": 150 | return this.createColorInput(hudField, onDataChange, path); 151 | case "BOOLEAN": 152 | return this.createCheckBox(hudField, onDataChange, path); 153 | case "NUMERIC": 154 | return this.createInput(hudField, onDataChange, path); 155 | } 156 | } 157 | 158 | createColorInput(hudField: HUDField, onDataChange: Function, path: string) { 159 | const key = Utils.uuidv4(); 160 | const {label} = hudField; 161 | const defaultValue = String(this.getFieldValue(hudField, path)); 162 | this.setConfigState(path, defaultValue); 163 | return ( 164 | ) 166 | } 167 | 168 | createInput(hudField: HUDField, onDataChange: Function, path: string) { 169 | const key = Utils.uuidv4(); 170 | const {label, step, min, max} = hudField; 171 | const defaultValue = this.getFieldValue(hudField, path); 172 | this.setConfigState(path, defaultValue); 173 | return ( 174 | 179 | ) 180 | } 181 | 182 | createCheckBox(hudField: HUDField, onDataChange: Function, path: string) { 183 | const key = Utils.uuidv4(); 184 | const {label} = hudField; 185 | const defaultValue = this.getFieldValue(hudField, path); 186 | this.setConfigState(path, defaultValue); 187 | return ( 188 | 190 | ) 191 | } 192 | 193 | handleFileRead() { 194 | const content = this.fileReader.result; 195 | this.data = Utils.fromXML(content); 196 | this.buildElements(); 197 | }; 198 | 199 | handleFileChosen(e: any) { 200 | this.fileReader = new FileReader(); 201 | this.fileReader.onloadend = this.handleFileRead; 202 | this.fileReader.readAsText(e.target.files[0]); 203 | } 204 | 205 | buildElements() { 206 | const schemaKeys = Object.keys(this.schema); 207 | const htmlElements = []; 208 | for (const key of schemaKeys) { 209 | const path = key; 210 | this.setConfigState(path, {}); 211 | const elements: Array = []; 212 | const schemaElement: HUDElement = this.schema[key]; 213 | this.processHudElement(schemaElement, elements, path); 214 | htmlElements.push( 215 |
216 | {elements} 217 |
218 | ) 219 | } 220 | this.setState({ 221 | htmlElements 222 | }); 223 | } 224 | 225 | componentDidMount() { 226 | gameApiService.hudEditorConfig().then((schema) => { 227 | this.schema = schema; 228 | console.log(schema); 229 | this.buildElements(); 230 | }); 231 | } 232 | 233 | render() { 234 | return ( 235 |
236 |
237 |

UI config for HUDEditor!

238 | 242 | 244 | {this.state.htmlElements} 245 |
246 |
247 | ); 248 | } 249 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 2.3.2.RELEASE 11 | 12 | 13 | 14 | com.manson 15 | fo76tradeServer 16 | 1.3-SNAPSHOT 17 | 18 | 19 | 20 | jitpack.io 21 | https://jitpack.io 22 | 23 | 24 | central 25 | Maven Central 26 | default 27 | https://repo1.maven.org/maven2 28 | 29 | false 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.projectlombok 37 | lombok 38 | 1.18.16 39 | provided 40 | 41 | 42 | com.github.sdaskaliesku 43 | fo76Domain 44 | domain-1.11.0 45 | 46 | 47 | com.github.opendevl 48 | json2flat 49 | 1.0.3 50 | 51 | 52 | org.glassfish.jersey.core 53 | jersey-client 54 | 55 | 56 | org.glassfish.jersey.media 57 | jersey-media-jaxb 58 | 59 | 60 | org.glassfish.jersey.media 61 | jersey-media-json-jackson 62 | 63 | 64 | org.glassfish.jersey.inject 65 | jersey-hk2 66 | 67 | 68 | org.glassfish.jersey.media 69 | jersey-media-multipart 70 | 71 | 72 | 73 | com.google.guava 74 | guava 75 | 30.1.1-jre 76 | 77 | 78 | io.springfox 79 | springfox-boot-starter 80 | 3.0.0 81 | 82 | 83 | io.springfox 84 | springfox-data-rest 85 | 3.0.0 86 | 87 | 88 | io.springfox 89 | springfox-swagger2 90 | 3.0.0 91 | 92 | 93 | io.springfox 94 | springfox-swagger-ui 95 | 3.0.0 96 | 97 | 98 | org.springframework.boot 99 | spring-boot-starter-data-mongodb 100 | 101 | 102 | org.springframework.boot 103 | spring-boot-starter 104 | 105 | 106 | org.springframework.boot 107 | spring-boot-starter-web 108 | 109 | 110 | org.springframework.boot 111 | spring-boot-starter-test 112 | test 113 | 114 | 115 | org.junit.vintage 116 | junit-vintage-engine 117 | 118 | 119 | 120 | 121 | com.fasterxml.jackson.core 122 | jackson-core 123 | ${jackson.version} 124 | 125 | 126 | com.fasterxml.jackson.dataformat 127 | jackson-dataformat-xml 128 | ${jackson.version} 129 | 130 | 131 | com.github.javadev 132 | underscore 133 | 1.57 134 | 135 | 136 | org.apache.commons 137 | commons-lang3 138 | 3.11 139 | 140 | 141 | 142 | com.fasterxml.jackson.core 143 | jackson-databind 144 | ${jackson.version} 145 | 146 | 147 | org.apache.commons 148 | commons-collections4 149 | 4.4 150 | 151 | 152 | io.jsonwebtoken 153 | jjwt 154 | 0.9.1 155 | 156 | 157 | org.jetbrains.kotlin 158 | kotlin-reflect 159 | ${kotlin.version} 160 | test 161 | 162 | 163 | org.jetbrains.kotlin 164 | kotlin-test 165 | ${kotlin.version} 166 | test 167 | 168 | 169 | 170 | org.hibernate 171 | hibernate-core 172 | ${hibernate.version} 173 | test 174 | 175 | 176 | 177 | 178 | org.xerial 179 | sqlite-jdbc 180 | 3.20.1 181 | test 182 | 183 | 184 | com.zsoltfabok 185 | sqlite-dialect 186 | 1.0 187 | test 188 | 189 | 190 | 191 | 192 | 193 | 2.11.2 194 | UTF-8 195 | 1.8 196 | 1.8 197 | 1.8 198 | 1.4.21 199 | 5.4.27.Final 200 | 201 | 202 | 203 | 204 | 205 | pl.project13.maven 206 | git-commit-id-plugin 207 | 208 | false 209 | false 210 | false 211 | yyyy-MM-dd HH:mm:ss 212 | 213 | 214 | 215 | com.github.eirslett 216 | frontend-maven-plugin 217 | 1.9.1 218 | 219 | target 220 | 221 | 222 | 223 | install node and npm 224 | 225 | install-node-and-npm 226 | 227 | 228 | v12.14.0 229 | 6.13.4 230 | 231 | 232 | 233 | npm install 234 | 235 | npm 236 | 237 | 238 | install 239 | 240 | 241 | 242 | webpack build 243 | 244 | npm 245 | 246 | 247 | run build 248 | 249 | 250 | 251 | 252 | 253 | 254 | org.springframework.boot 255 | spring-boot-maven-plugin 256 | 257 | 258 | 259 | build-info 260 | 261 | 262 | 263 | 264 | 265 | org.jetbrains.kotlin 266 | kotlin-maven-plugin 267 | ${kotlin.version} 268 | 269 | 270 | test-compile 271 | test-compile 272 | 273 | test-compile 274 | 275 | 276 | 277 | 278 | 1.8 279 | 280 | 281 | 282 | org.apache.maven.plugins 283 | maven-compiler-plugin 284 | 285 | 286 | compile 287 | compile 288 | 289 | compile 290 | 291 | 292 | 293 | testCompile 294 | test-compile 295 | 296 | testCompile 297 | 298 | 299 | 300 | 301 | 302 | 303 | -------------------------------------------------------------------------------- /src/main/ui/component/config/pipboySchema.ts: -------------------------------------------------------------------------------- 1 | import {ItemTypes, MatchModes, PipboyActions} from "./configs"; 2 | import {JsonSchema} from "@jsonforms/core"; 3 | import {as3KeyCodes, getAs3CharCode, LayoutTypes, Type} from "./schema"; 4 | 5 | const Actions = PipboyActions; 6 | 7 | const schema: JsonSchema = { 8 | type: 'object', 9 | properties: { 10 | debug: { 11 | type: 'boolean', 12 | title: 'Debug mode', 13 | }, 14 | showRealItemName: { 15 | type: 'boolean', 16 | title: 'Show pop-up with real item name', 17 | description: 'Once you select(highlight) item, it will show a message with full real item name' 18 | }, 19 | delayConfig: { 20 | type: 'object', 21 | title: 'Delay configuration', 22 | default: { 23 | drop: { 24 | initialDelay: 100, 25 | step: 20 26 | }, 27 | consume: { 28 | initialDelay: 100, 29 | step: 20 30 | }, 31 | }, 32 | properties: { 33 | drop: { 34 | type: 'object', 35 | properties: { 36 | initialDelay: { 37 | type: 'number', 38 | title: 'Initial delay', 39 | default: 100 40 | }, 41 | step: { 42 | type: 'number', 43 | title: 'Delay increase step (per each item)', 44 | default: 20 45 | } 46 | } 47 | }, 48 | consume: { 49 | type: 'object', 50 | properties: { 51 | initialDelay: { 52 | type: 'number', 53 | title: 'Initial delay', 54 | default: 100 55 | }, 56 | step: { 57 | type: 'number', 58 | title: 'Delay increase step (per each item)', 59 | default: 20 60 | } 61 | } 62 | } 63 | } 64 | }, 65 | configs: { 66 | type: 'array', 67 | title: 'Configurations', 68 | items: { 69 | type: 'object', 70 | properties: { 71 | name: { 72 | type: "string", 73 | title: 'Config name', 74 | default: 'some config name' 75 | }, 76 | enabled: { 77 | type: "boolean", 78 | title: 'Enabled', 79 | default: true, 80 | description: 'Enables specific configuration section' 81 | }, 82 | hotkey: { 83 | type: "string", 84 | enum: Object.keys(as3KeyCodes), 85 | title: 'Hotkey', 86 | description: 'Hotkey that will trigger listed actions', 87 | }, 88 | charConfig: { 89 | type: 'object', 90 | properties: { 91 | enabled: { 92 | type: "boolean", 93 | title: 'Verify character name', 94 | description: 'If enabled - mod will check whether current character name matches specified' 95 | }, 96 | name: { 97 | type: "string", 98 | title: 'Character name', 99 | }, 100 | } 101 | }, 102 | teenoodleTragedyProtection: { 103 | type: 'object', 104 | title: 'Teenoodle tragedy protection', 105 | properties: { 106 | ignoreConfig: { 107 | type: 'object', 108 | properties: { 109 | legendary: { 110 | title: 'Prevent any mod actions on legendary items', 111 | type: "boolean", 112 | default: true 113 | }, 114 | nonTradable: { 115 | title: 'Prevent any mod actions on non-tradable items', 116 | type: "boolean", 117 | default: true 118 | }, 119 | } 120 | }, 121 | excludedItems: { 122 | default: [ 123 | { 124 | name: 'some item name', 125 | matchMode: 'CONTAINS' 126 | } 127 | ], 128 | items: { 129 | type: 'object', 130 | properties: { 131 | name: { 132 | type: "string", 133 | title: 'Item name to ignore (prevent any mod actions)', 134 | default: 'item name' 135 | }, 136 | matchMode: { 137 | type: "string", 138 | enum: MatchModes, 139 | title: 'Match mode', 140 | description: 'Used to match specified item name and item name in game', 141 | default: 'CONTAINS' 142 | }, 143 | } 144 | } 145 | } 146 | } 147 | }, 148 | itemConfigs: { 149 | type: "array", 150 | title: 'Item actions', 151 | items: { 152 | type: 'object', 153 | title: '', 154 | properties: { 155 | name: { 156 | title: 'Item name', 157 | description: 'Item name to perform action on', 158 | default: 'some item name', 159 | type: 'string' 160 | }, 161 | enabled: { 162 | type: "boolean", 163 | title: 'Enabled', 164 | default: true, 165 | description: 'Enables specific configuration' 166 | }, 167 | action: { 168 | type: 'string', 169 | enum: Actions, 170 | default: 'CONSUME', 171 | title: 'Action', 172 | description: 'Action that should be performed on item' 173 | }, 174 | type: { 175 | type: 'string', 176 | title: 'Item type', 177 | description: 'Item type to perform action on', 178 | default: 'JUNK', 179 | enum: ItemTypes, 180 | }, 181 | quantity: { 182 | type: 'number', 183 | default: 1, 184 | title: 'Amount of items to perform action on' 185 | }, 186 | matchMode: { 187 | type: "string", 188 | enum: MatchModes, 189 | title: 'Match mode', 190 | description: 'Used to match specified item name and item name in game', 191 | default: 'EXACT' 192 | }, 193 | } 194 | }, 195 | default: [ 196 | { 197 | name: 'some item name', 198 | type: 'AMMO', 199 | quantity: 1, 200 | matchMode: 'EXACT', 201 | action: 'DROP', 202 | enabled: true 203 | } 204 | ] 205 | }, 206 | } 207 | } 208 | } 209 | }, 210 | }; 211 | 212 | const uischema: any = { 213 | type: LayoutTypes.VerticalLayout, 214 | scope: '', 215 | elements: [ 216 | { 217 | type: Type.Control, 218 | scope: '#/properties/debug', 219 | }, 220 | { 221 | type: Type.Control, 222 | scope: '#/properties/showRealItemName', 223 | }, 224 | { 225 | type: Type.Control, 226 | scope: '#/properties/configs', 227 | options: { 228 | detail: { 229 | type: LayoutTypes.VerticalLayout, 230 | elements: [ 231 | { 232 | type: Type.Control, 233 | scope: '#/properties/enabled' 234 | }, 235 | { 236 | type: Type.Control, 237 | scope: '#/properties/name' 238 | }, 239 | { 240 | type: Type.Control, 241 | scope: '#/properties/hotkey' 242 | }, 243 | { 244 | type: Type.Control, 245 | scope: '#/properties/charConfig', 246 | options: { 247 | detail: { 248 | type: LayoutTypes.HorizontalLayout, 249 | elements: [ 250 | { 251 | type: Type.Control, 252 | scope: '#/properties/enabled' 253 | }, 254 | { 255 | type: Type.Control, 256 | scope: '#/properties/name' 257 | }, 258 | ] 259 | } 260 | } 261 | }, 262 | { 263 | type: Type.Control, 264 | scope: '#/properties/teenoodleTragedyProtection', 265 | options: { 266 | detail: { 267 | type: LayoutTypes.VerticalLayout, 268 | elements: [ 269 | { 270 | type: Type.Control, 271 | scope: '#/properties/ignoreConfig', 272 | options: { 273 | detail: { 274 | type: LayoutTypes.HorizontalLayout, 275 | elements: [ 276 | { 277 | type: Type.Control, 278 | scope: '#/properties/legendary' 279 | }, 280 | { 281 | type: Type.Control, 282 | scope: '#/properties/nonTradable' 283 | }, 284 | ] 285 | } 286 | } 287 | }, 288 | { 289 | type: Type.Control, 290 | scope: '#/properties/excludedItems' 291 | }, 292 | ] 293 | } 294 | } 295 | }, 296 | { 297 | type: Type.Control, 298 | scope: '#/properties/itemConfigs' 299 | }, 300 | ] 301 | } 302 | } 303 | }, 304 | { 305 | type: Type.Control, 306 | scope: '#/properties/delayConfig', 307 | options: { 308 | detail: { 309 | type: LayoutTypes.HorizontalLayout, 310 | elements: [ 311 | { 312 | type: "Control", 313 | scope: "#/properties/drop" 314 | }, 315 | { 316 | type: "Control", 317 | scope: "#/properties/consume" 318 | }, 319 | ] 320 | } 321 | } 322 | } 323 | ] 324 | }; 325 | 326 | 327 | export const inventOmaticPipboySchema = { 328 | uischema: uischema, 329 | schema: schema, 330 | defaultConfig: { 331 | debug: false, 332 | showRealItemName: false, 333 | configs: [ 334 | { 335 | name: 'Sample config', 336 | hotkey: getAs3CharCode("79"), 337 | itemConfigs: [ 338 | { 339 | name: 'Lunchbox', 340 | type: 'AID', 341 | quantity: 1, 342 | matchMode: 'EXACT', 343 | action: 'CONSUME', 344 | enabled: true 345 | } 346 | ], 347 | enabled: true, 348 | charConfig: { 349 | enabled: false, 350 | name: '', 351 | }, 352 | teenoodleTragedyProtection: { 353 | ignoreConfig: { 354 | legendary: true, 355 | nonTradable: true 356 | }, 357 | excludedItems: [ 358 | { 359 | name: 'bloodied', 360 | matchMode: 'CONTAINS' 361 | } 362 | ] 363 | } 364 | } 365 | ], 366 | }, 367 | name: 'Invent-O-Matic-Pipboy', 368 | fileName: 'inventOmaticPipboyConfig.json', 369 | } --------------------------------------------------------------------------------