├── tests ├── .gitkeep ├── TestCase.php ├── Api │ ├── Endpoint │ │ ├── TransfermarketEndpointTest.php │ │ └── EndpointTestCase.php │ ├── CoreTest.php │ └── PinTest.php ├── Config │ └── ConfigTest.php └── Authentication │ └── CredentialsTest.php ├── .gitignore ├── .env.dist ├── src ├── Api │ ├── Core.php │ ├── Pin.php │ └── CoreInterface.php ├── bootstrap.php ├── Resources │ └── locales │ │ ├── en_US.yaml │ │ └── de_DE.yaml ├── Http │ ├── CookieJarBuilderInterface.php │ ├── ClientFactoryInterface.php │ ├── Plugin │ │ └── ClientCallPlugin.php │ ├── ClientCall.php │ ├── CookieJarBuilder.php │ └── ClientFactory.php ├── Exception │ ├── ServerDownException.php │ ├── AuthFailedException.php │ ├── CaptchaException.php │ ├── PermissionDeniedException.php │ ├── UserExpiredException.php │ ├── MaxSessionsException.php │ ├── NoPersonaException.php │ ├── SessionExpiredException.php │ ├── ToManyRequestsException.php │ ├── PinErrorException.php │ ├── ProvideSecurityCodeException.php │ ├── TemporaryBanException.php │ ├── IncorrectCredentialsException.php │ ├── NoSessionException.php │ ├── IncorrectSecurityCodeException.php │ ├── TransferMarketDisabledException.php │ ├── FutFailedException.php │ ├── FutException.php │ └── FutResponseException.php ├── Items │ ├── CurrencyValue.php │ ├── Kit.php │ ├── DuplicateItem.php │ ├── Health.php │ ├── ItemInterface.php │ ├── Contract.php │ ├── TradeItemInterface.php │ ├── SuperBase.php │ ├── Item.php │ ├── TradeItem.php │ └── Player.php ├── Model │ ├── ProxyInterface.php │ └── Proxy.php ├── Authentication │ ├── AccountInterface.php │ ├── SessionInterface.php │ ├── CredentialsInterface.php │ ├── Account.php │ ├── Credentials.php │ └── Session.php ├── Response │ ├── AbstractResponse.php │ ├── WatchlistResponse.php │ ├── UnassignedResponse.php │ ├── TradepileResponse.php │ ├── MarketSearchResponse.php │ ├── BidResponse.php │ └── TradeStatusResponse.php ├── Config │ ├── ConfigInterface.php │ └── Config.php ├── Locale │ └── Locale.php ├── Util │ ├── FutUtil.php │ └── EAHasher.php └── Mapper │ └── Mapper.php ├── data └── fixtures │ ├── weekendleage_status.respsonse.json │ ├── trade_status_lite.response.json │ ├── club_stats_stuff.respsonse.json │ ├── unassigned.response.json │ ├── accountinfo.response.json │ ├── sets_challenges.response.json │ ├── bid.response.json │ ├── watchlist.reponse.json │ ├── tradepile.response.json │ ├── settings.response.json │ └── sets_challenge.response.json ├── phpstan.neon.dist ├── composer-require-checker.json ├── .github ├── workflows │ └── php.yml └── FUNDING.yml ├── phpunit.xml ├── phpcs.xml ├── composer.json └── README.md /tests/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | vendor/ 3 | .phpunit.result.cache 4 | .env -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | EMAIL=email@example.com 2 | PASSWORD=YOUR_ACCOUNT_PASSWORD 3 | PLATFORM=ps4 -------------------------------------------------------------------------------- /src/Api/Core.php: -------------------------------------------------------------------------------- 1 | load(__DIR__ . '/../.env'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /composer-require-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol-whitelist": [ 3 | "null", 4 | "true", 5 | "false", 6 | "static", 7 | "self", 8 | "parent", 9 | "array", 10 | "string", 11 | "int", 12 | "float", 13 | "bool", 14 | "iterable", 15 | "callable", 16 | "void", 17 | "object" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/Exception/ServerDownException.php: -------------------------------------------------------------------------------- 1 | get('name'); 12 | } 13 | 14 | public function getFunds() : int 15 | { 16 | return $this->get('funds'); 17 | } 18 | 19 | public function getFinalFunds() : int 20 | { 21 | return $this->get('finalFunds'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Model/ProxyInterface.php: -------------------------------------------------------------------------------- 1 | rawBody = $rawBody; 18 | } 19 | 20 | /** 21 | * @return mixed[] 22 | */ 23 | public function getRawBody() : array 24 | { 25 | return $this->rawBody; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Items/Kit.php: -------------------------------------------------------------------------------- 1 | get('assetId'); 12 | } 13 | 14 | public function getRating() : ?int 15 | { 16 | return $this->get('rating'); 17 | } 18 | 19 | public function getCategory() : ?string 20 | { 21 | return $this->get('category'); 22 | } 23 | 24 | public function getName() : ?string 25 | { 26 | return $this->get('name'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v1 18 | 19 | - name: Validate composer.json and composer.lock 20 | run: composer validate 21 | 22 | - name: Install dependencies 23 | run: composer install --prefer-dist --no-progress --no-suggest 24 | 25 | - name: Copy env 26 | run: cp .env.dist .env 27 | 28 | - name: Run Check 29 | run: composer check-build 30 | -------------------------------------------------------------------------------- /src/Authentication/SessionInterface.php: -------------------------------------------------------------------------------- 1 | itemId = $itemId; 18 | $this->duplicateItemId = $duplicateItemId; 19 | } 20 | 21 | public function getItemId() : int 22 | { 23 | return $this->itemId; 24 | } 25 | 26 | public function getDuplicateItemId() : int 27 | { 28 | return $this->duplicateItemId; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Items/Health.php: -------------------------------------------------------------------------------- 1 | get('assetId'); 12 | } 13 | 14 | public function getRating() : ?int 15 | { 16 | return $this->get('rating'); 17 | } 18 | 19 | public function getCardAssetId() : ?int 20 | { 21 | return $this->get('cardassetid'); 22 | } 23 | 24 | public function getWeightRare() : ?int 25 | { 26 | return $this->get('weightrare'); 27 | } 28 | 29 | public function getAmount() : ?int 30 | { 31 | return $this->get('amount'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Exception/FutFailedException.php: -------------------------------------------------------------------------------- 1 | getErrorMessage(), $response, $this->getErrorReason(), $options, $previous); 18 | } 19 | 20 | abstract protected function getErrorMessage() : string; 21 | 22 | abstract protected function getErrorReason() : string; 23 | } 24 | -------------------------------------------------------------------------------- /src/Items/ItemInterface.php: -------------------------------------------------------------------------------- 1 | createClientFactoryMock('transfermarekt.response.json'); 14 | $core = $this->createCore($factory); 15 | 16 | $result = $core->search(); 17 | 18 | self::assertCount(21, $result->getAuctions()); 19 | self::assertCount(0, $result->getBidTokens()); 20 | self::assertNotEmpty($result->getRawBody()); 21 | self::assertContainsOnlyInstancesOf(TradeItem::class, $result->getAuctions()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | tests 14 | 15 | 16 | 17 | 18 | 19 | src 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Exception/FutException.php: -------------------------------------------------------------------------------- 1 | options = $options; 23 | } 24 | 25 | /** 26 | * @return mixed[] 27 | */ 28 | public function getOptions() : array 29 | { 30 | return $this->options; 31 | } 32 | 33 | /** 34 | * @return mixed|null 35 | */ 36 | public function getOption(string $name) 37 | { 38 | return $this->options[$name] ?? null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Items/Contract.php: -------------------------------------------------------------------------------- 1 | get('rating'); 12 | } 13 | 14 | public function getAssetId() : ?int 15 | { 16 | return $this->get('assetId'); 17 | } 18 | 19 | public function getCardAssetId() : ?int 20 | { 21 | return $this->get('cardassetid'); 22 | } 23 | 24 | public function getWeightRare() : ?int 25 | { 26 | return $this->get('weightrare'); 27 | } 28 | 29 | public function getBronze() : ?int 30 | { 31 | return $this->get('bronze'); 32 | } 33 | 34 | public function getSilver() : ?int 35 | { 36 | return $this->get('silver'); 37 | } 38 | 39 | public function getGold() : ?int 40 | { 41 | return $this->get('gold'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Locale/Locale.php: -------------------------------------------------------------------------------- 1 | translator = new Translator($locale); 21 | 22 | $path = __DIR__ . '/../Resources/locales/'; 23 | $file = $path . $locale . '.yaml'; 24 | 25 | $this->translator->addLoader('yaml', new YamlFileLoader()); 26 | $this->translator->addResource('yaml', $file, $locale); 27 | } 28 | 29 | /** 30 | * @param mixed $value 31 | */ 32 | public function get($value) : string 33 | { 34 | return $this->translator->trans($value); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Authentication/CredentialsInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | src/ 10 | tests/ 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Response/WatchlistResponse.php: -------------------------------------------------------------------------------- 1 | credits = $credits; 26 | $this->auctions = $auctions; 27 | $this->total = $total; 28 | } 29 | 30 | public function getCredits() : ?int 31 | { 32 | return $this->credits; 33 | } 34 | 35 | /** 36 | * @return TradeItem[] 37 | */ 38 | public function getAuctions() : array 39 | { 40 | return $this->auctions; 41 | } 42 | 43 | public function getTotal() : ?int 44 | { 45 | return $this->total; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Response/UnassignedResponse.php: -------------------------------------------------------------------------------- 1 | items = $items; 25 | $this->duplicateItemIdList = $duplicateItemIdList; 26 | } 27 | 28 | /** 29 | * @return ItemInterface[] 30 | */ 31 | public function getItems() : array 32 | { 33 | return $this->items; 34 | } 35 | 36 | /** 37 | * @return DuplicateItem[] 38 | */ 39 | public function getDuplicateItemIdList() : array 40 | { 41 | return $this->duplicateItemIdList; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Items/TradeItemInterface.php: -------------------------------------------------------------------------------- 1 | credits = $credits; 27 | $this->auctions = $auctions; 28 | $this->bidTokens = $bidTokens; 29 | } 30 | 31 | public function getCredits() : ?int 32 | { 33 | return $this->credits; 34 | } 35 | 36 | /** 37 | * @return TradeItem[] 38 | */ 39 | public function getAuctions() : array 40 | { 41 | return $this->auctions; 42 | } 43 | 44 | /** 45 | * @return mixed[] 46 | */ 47 | public function getBidTokens() : array 48 | { 49 | return $this->bidTokens; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Http/Plugin/ClientCallPlugin.php: -------------------------------------------------------------------------------- 1 | call = $call; 22 | } 23 | 24 | public function handleRequest(RequestInterface $request, callable $next, callable $first) : Promise 25 | { 26 | $this->call->setRequest($request); 27 | 28 | return $next($request)->then(function (ResponseInterface $response) { 29 | $this->call->setResponse($response); 30 | 31 | return $response; 32 | }, function (Exception $exception) : void { 33 | if ($exception instanceof Exception\HttpException) { 34 | $this->call->setResponse($exception->getResponse()); 35 | } 36 | 37 | throw $exception; 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /data/fixtures/trade_status_lite.response.json: -------------------------------------------------------------------------------- 1 | { 2 | "auctionInfo": [ 3 | { 4 | "tradeId": 261540141928, 5 | "itemData": { 6 | "id": 368363499429, 7 | "timestamp": 0, 8 | "untradeable": false, 9 | "resourceId": 0, 10 | "owners": 0, 11 | "cardsubtypeid": 0, 12 | "lastSalePrice": 0, 13 | "morale": 0, 14 | "fitness": 0, 15 | "injuryGames": 0, 16 | "statsList": [], 17 | "lifetimeStats": [], 18 | "training": 0, 19 | "contract": 0, 20 | "suspension": 0, 21 | "attributeList": [], 22 | "pile": 0, 23 | "nation": 0, 24 | "resourceGameYear": 2020 25 | }, 26 | "tradeState": "active", 27 | "buyNowPrice": 1784000, 28 | "currentBid": 0, 29 | "watched": false, 30 | "bidState": "none", 31 | "startingBid": 1744000, 32 | "expires": 60, 33 | "tradeIdStr": "261540141928", 34 | "tradeOwner": false 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /src/Http/ClientCall.php: -------------------------------------------------------------------------------- 1 | request; 24 | } 25 | 26 | public function setRequest(RequestInterface $request) : void 27 | { 28 | $this->request = $request; 29 | } 30 | 31 | public function getResponse() : ResponseInterface 32 | { 33 | return $this->response; 34 | } 35 | 36 | public function setResponse(ResponseInterface $response) : void 37 | { 38 | $this->response = $response; 39 | } 40 | 41 | /** 42 | * @return mixed 43 | */ 44 | public function getContent() 45 | { 46 | if ($this->contents === null) { 47 | $this->contents = $this->getResponse()->getBody()->getContents(); 48 | } 49 | 50 | return $this->contents; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Exception/FutResponseException.php: -------------------------------------------------------------------------------- 1 | getStatusCode(); 30 | } else { 31 | $code = 0; 32 | } 33 | 34 | parent::__construct($message, $options, $code, $previous); 35 | 36 | $this->response = $response; 37 | $this->reason = $reason; 38 | } 39 | 40 | public function getReason() : ?string 41 | { 42 | return $this->reason; 43 | } 44 | 45 | public function getResponse() : ?ResponseInterface 46 | { 47 | return $this->response; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Api/CoreTest.php: -------------------------------------------------------------------------------- 1 | expectException(ProvideSecurityCodeException::class), 31 | $this->expectException(IncorrectCredentialsException::class) 32 | ); 33 | 34 | self::logicalXor( 35 | $this->expectExceptionMessage('You must provide a backup code'), 36 | $this->expectExceptionMessage('Your email or password is incorrect.') 37 | ); 38 | 39 | $core->login(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Http/CookieJarBuilder.php: -------------------------------------------------------------------------------- 1 | getCredentials()->getEmail(); 22 | 23 | if (array_key_exists($email, $this->jars) === false) { 24 | $this->jars[$email] = $this->createFileCookieJarByTemp($account->getCredentials()->getEmail()); 25 | } 26 | 27 | return $this->jars[$email]; 28 | } 29 | 30 | private function createFileCookieJarByTemp(string $email) : CookieJarInterface 31 | { 32 | $filename = sys_get_temp_dir() . '/' . sha1($email); 33 | 34 | return $this->createFileCookieJarByFilename($filename); 35 | } 36 | 37 | private function createFileCookieJarByFilename(string $filename) : CookieJarInterface 38 | { 39 | return new FileCookieJar($filename, true); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Response/MarketSearchResponse.php: -------------------------------------------------------------------------------- 1 | auctions = $auctions; 34 | $this->bidTokens = $bidTokens; 35 | } 36 | 37 | /** 38 | * @return TradeItem[] 39 | */ 40 | public function getAuctions() : array 41 | { 42 | return $this->auctions; 43 | } 44 | 45 | public function hasAuctions() : bool 46 | { 47 | return count($this->auctions) > 0; 48 | } 49 | 50 | /** 51 | * @return mixed[] 52 | */ 53 | public function getBidTokens() : array 54 | { 55 | return $this->bidTokens; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Response/BidResponse.php: -------------------------------------------------------------------------------- 1 | auctions = $auctions; 25 | $this->credits = $credits; 26 | } 27 | 28 | /** 29 | * @return TradeItem[] 30 | */ 31 | public function getAuctions() : array 32 | { 33 | return $this->auctions; 34 | } 35 | 36 | public function hasAuctions() : bool 37 | { 38 | return count($this->auctions) > 0; 39 | } 40 | 41 | public function hasAuction(int $index) : bool 42 | { 43 | return isset($this->auctions[$index]); 44 | } 45 | 46 | public function getAuction(int $index) : ?TradeItemInterface 47 | { 48 | if (! $this->hasAuction($index)) { 49 | return null; 50 | } 51 | 52 | return $this->auctions[$index]; 53 | } 54 | 55 | public function getCredits() : ?int 56 | { 57 | return $this->credits; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /data/fixtures/club_stats_stuff.respsonse.json: -------------------------------------------------------------------------------- 1 | { 2 | "bonus": [ 3 | { 4 | "type": "defending", 5 | "value": 15 6 | }, 7 | { 8 | "type": "dribbling", 9 | "value": 10 10 | }, 11 | { 12 | "type": "pace", 13 | "value": 20 14 | }, 15 | { 16 | "type": "heading", 17 | "value": 5 18 | }, 19 | { 20 | "type": "passing", 21 | "value": 10 22 | }, 23 | { 24 | "type": "fitness", 25 | "value": 18 26 | }, 27 | { 28 | "type": "gkReflexes", 29 | "value": 5 30 | }, 31 | { 32 | "type": "gkHandling", 33 | "value": 5 34 | }, 35 | { 36 | "type": "gkOneOnOne", 37 | "value": 5 38 | }, 39 | { 40 | "type": "gkPositioning", 41 | "value": 10 42 | }, 43 | { 44 | "type": "contract", 45 | "value": 7 46 | }, 47 | { 48 | "type": "managerTalk", 49 | "value": 0 50 | }, 51 | { 52 | "type": "physioLeg", 53 | "value": 25 54 | }, 55 | { 56 | "type": "physioBack", 57 | "value": 15 58 | }, 59 | { 60 | "type": "physioHip", 61 | "value": 5 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /src/Authentication/Account.php: -------------------------------------------------------------------------------- 1 | credentials = $credentials; 30 | $this->proxy = $proxy; 31 | $this->session = $session; 32 | } 33 | 34 | public function getCredentials() : CredentialsInterface 35 | { 36 | return $this->credentials; 37 | } 38 | 39 | public function getSession() : ?SessionInterface 40 | { 41 | return $this->session; 42 | } 43 | 44 | public function setSession(SessionInterface $session) : void 45 | { 46 | $this->session = $session; 47 | } 48 | 49 | public function resetSession() : void 50 | { 51 | $this->session = null; 52 | } 53 | 54 | public function getProxy() : ?ProxyInterface 55 | { 56 | return $this->proxy; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Util/FutUtil.php: -------------------------------------------------------------------------------- 1 | 'FFA20PCC', 13 | 'xbox' => 'FFA20XBO', 14 | 'xbox360' => 'FFA20XBX', 15 | 'ps3' => 'FFA20PS3', 16 | 'ps4' => 'FFA20PS4', 17 | ]; 18 | 19 | private function __construct() 20 | { 21 | // object not allowed 22 | } 23 | 24 | public static function getBaseId(int $assetId) : int 25 | { 26 | $version = 0; 27 | $assetId += 0xC4000000; 28 | while ($assetId > 0x01000000) { 29 | $version++; 30 | if ($version === 1) { 31 | //the constant applied to all items 32 | $assetId -= 1342177280; 33 | } elseif ($version === 2) { 34 | //the value added to the first updated version 35 | $assetId -= 50331648; 36 | } else { 37 | //the value added on all subsequent versions 38 | $assetId -= 16777216; 39 | } 40 | } 41 | 42 | return $assetId; 43 | } 44 | 45 | public static function getGameSku(string $platform) : string 46 | { 47 | if (! isset(self::GAME_SKU[$platform])) { 48 | throw new FutException('Wrong platform. (Valid ones are pc/xbox/xbox360/ps3/ps4)'); 49 | } 50 | 51 | return self::GAME_SKU[$platform]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Config/ConfigTest.php: -------------------------------------------------------------------------------- 1 | getOptions(); 17 | 18 | $keys = [ 19 | 'requestProtocol', 20 | 'authURL', 21 | 'eadpConnectHost', 22 | 'eadpPortalHost', 23 | 'eadpProxyHost', 24 | 'eadpClientId', 25 | 'eadpClientSecret', 26 | 'pinURL', 27 | 'releaseType', 28 | 'showOffURL', 29 | 'resourceRoot', 30 | 'resourceBase', 31 | 'changelist', 32 | 'requestTimeout', 33 | 'localStorageVersion', 34 | 'maxConsecutive500Errors', 35 | 'settingsRefreshInterval', 36 | 'verboseLogging', 37 | 'staticResponseData', 38 | 'originCss', 39 | 'originJS', 40 | 'originHost', 41 | 'originProfile', 42 | 'originMasterTitle', 43 | 'funCaptchaPublicKey', 44 | 'userAgent', 45 | 'delay', 46 | 'delayMinTime', 47 | 'delayMaxTime', 48 | ]; 49 | 50 | self::assertCount(29, $options); 51 | 52 | foreach ($keys as $key) { 53 | self::assertArrayHasKey($key, $options); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Items/SuperBase.php: -------------------------------------------------------------------------------- 1 | data = $data; 21 | } 22 | 23 | /** 24 | * @return mixed[] 25 | */ 26 | public function getArray() : array 27 | { 28 | return $this->data; 29 | } 30 | 31 | /** 32 | * @param mixed $key 33 | * 34 | * @return mixed 35 | */ 36 | public function get($key) 37 | { 38 | return $this->data[$key] ?? null; 39 | } 40 | 41 | /** 42 | * @param mixed $offset 43 | */ 44 | public function offsetExists($offset) : bool 45 | { 46 | return isset($this->data[$offset]) || array_key_exists($offset, $this->data); 47 | } 48 | 49 | /** 50 | * @param mixed $offset 51 | * 52 | * @return mixed 53 | */ 54 | public function offsetGet($offset) 55 | { 56 | return $this->get($offset); 57 | } 58 | 59 | /** 60 | * @param mixed $offset 61 | * @param mixed $value 62 | */ 63 | public function offsetSet($offset, $value) : void 64 | { 65 | $this->data[$offset] = $value; 66 | } 67 | 68 | /** 69 | * @param mixed $offset 70 | */ 71 | public function offsetUnset($offset) : void 72 | { 73 | if (! $this->offsetExists($offset)) { 74 | return; 75 | } 76 | 77 | unset($this->data[$offset]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Authentication/CredentialsTest.php: -------------------------------------------------------------------------------- 1 | expectNotToPerformAssertions(); 24 | } 25 | 26 | public function testCredentialFailure() : void 27 | { 28 | $this->expectException(FutException::class); 29 | $this->expectExceptionMessage('Wrong platform. (Valid ones are pc/xbox/xbox360/ps3/ps4)'); 30 | 31 | new Credentials( 32 | 'test@example.com', 33 | 'password', 34 | 'wrong_platform' 35 | ); 36 | } 37 | 38 | public function testCredentialEmail() : void 39 | { 40 | $this->expectException(InvalidArgumentException::class); 41 | $this->expectExceptionMessage('Expected a value to be a valid e-mail address. Got "wrong_email"'); 42 | 43 | new Credentials( 44 | 'wrong_email', 45 | 'password', 46 | CredentialsInterface::PLATFORM_PS4 47 | ); 48 | } 49 | 50 | public function testCredentialPassword() : void 51 | { 52 | $this->expectException(InvalidArgumentException::class); 53 | $this->expectExceptionMessage('Expected a non-empty value. Got: ""'); 54 | 55 | new Credentials( 56 | 'test@example.com', 57 | '', 58 | CredentialsInterface::PLATFORM_PS4 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /data/fixtures/unassigned.response.json: -------------------------------------------------------------------------------- 1 | { 2 | "itemData": [ 3 | { 4 | "id": 369798564985, 5 | "timestamp": 1572616187, 6 | "formation": "f5212", 7 | "untradeable": false, 8 | "assetId": 244791, 9 | "rating": 77, 10 | "itemType": "player", 11 | "resourceId": 244791, 12 | "owners": 3, 13 | "discardValue": 308, 14 | "itemState": "free", 15 | "cardsubtypeid": 2, 16 | "lastSalePrice": 400, 17 | "fitness": 99, 18 | "injuryType": "none", 19 | "injuryGames": 0, 20 | "preferredPosition": "CM", 21 | "training": 0, 22 | "contract": 7, 23 | "teamid": 673, 24 | "rareflag": 0, 25 | "playStyle": 250, 26 | "leagueId": 4, 27 | "assists": 0, 28 | "lifetimeAssists": 0, 29 | "loyaltyBonus": 0, 30 | "pile": 6, 31 | "nation": 43, 32 | "marketDataMinPrice": 350, 33 | "marketDataMaxPrice": 10000, 34 | "resourceGameYear": 2020, 35 | "attributeArray": [ 36 | 71, 37 | 68, 38 | 76, 39 | 76, 40 | 70, 41 | 68 42 | ], 43 | "statsArray": [ 44 | 0, 45 | 0, 46 | 0, 47 | 0, 48 | 0 49 | ], 50 | "lifetimeStatsArray": [ 51 | 0, 52 | 0, 53 | 0, 54 | 0, 55 | 0 56 | ], 57 | "skillmoves": 2, 58 | "weakfootabilitytypecode": 3, 59 | "attackingworkrate": 2, 60 | "defensiveworkrate": 2, 61 | "trait1": 3072, 62 | "trait2": 0, 63 | "preferredfoot": 1 64 | } 65 | ] 66 | } -------------------------------------------------------------------------------- /src/Items/Item.php: -------------------------------------------------------------------------------- 1 | get('id'); 14 | } 15 | 16 | public function getResourceId() : int 17 | { 18 | return $this->get('resourceId'); 19 | } 20 | 21 | public function isUntradeable() : bool 22 | { 23 | return $this->get('untradeable'); 24 | } 25 | 26 | public function getOwners() : int 27 | { 28 | return $this->get('owners'); 29 | } 30 | 31 | public function getDiscardValue() : int 32 | { 33 | return $this->get('discardValue'); 34 | } 35 | 36 | public function getLastSalePrice() : ?int 37 | { 38 | return $this->get('lastSalePrice'); 39 | } 40 | 41 | public function getItemState() : ?string 42 | { 43 | return $this->get('itemState'); 44 | } 45 | 46 | public function getCardSubTypeId() : ?int 47 | { 48 | return $this->get('cardsubtypeid'); 49 | } 50 | 51 | public function getPile() : ?string 52 | { 53 | return $this->get('pile'); 54 | } 55 | 56 | public function getMarketDataMinPrice() : ?int 57 | { 58 | return $this->get('marketDataMinPrice'); 59 | } 60 | 61 | public function getMarketDataMaxPrice() : ?int 62 | { 63 | return $this->get('marketDataMaxPrice'); 64 | } 65 | 66 | public function getTimestamp() : ?int 67 | { 68 | return $this->get('timestamp'); 69 | } 70 | 71 | public function getItemType() : string 72 | { 73 | return $this->get('itemType'); 74 | } 75 | 76 | public function getResourceGameYear() : ?int 77 | { 78 | return $this->get('resourceGameYear'); 79 | } 80 | 81 | public function getDateTime() : DateTime 82 | { 83 | $date = new DateTime(); 84 | 85 | if ($this->getTimestamp() !== null) { 86 | $date->setTimestamp($this->getTimestamp()); 87 | } 88 | 89 | return $date; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Response/TradeStatusResponse.php: -------------------------------------------------------------------------------- 1 | credits = $credits; 34 | $this->auctions = $auctions; 35 | $this->bidTokens = $bidTokens; 36 | $this->currencies = $currencies; 37 | } 38 | 39 | public function getCredits() : ?int 40 | { 41 | return $this->credits; 42 | } 43 | 44 | /** 45 | * @return TradeItemInterface[] 46 | */ 47 | public function getAuctions() : array 48 | { 49 | return $this->auctions; 50 | } 51 | 52 | public function hasAuctions() : bool 53 | { 54 | return count($this->auctions) > 0; 55 | } 56 | 57 | public function getAuction(int $index) : ?TradeItemInterface 58 | { 59 | if (! $this->hasAuction($index)) { 60 | return null; 61 | } 62 | 63 | return $this->auctions[$index]; 64 | } 65 | 66 | public function hasAuction(int $index) : bool 67 | { 68 | return array_key_exists($index, $this->auctions); 69 | } 70 | 71 | /** 72 | * @return mixed[] 73 | */ 74 | public function getBidTokens() : array 75 | { 76 | return $this->bidTokens; 77 | } 78 | 79 | /** 80 | * @return CurrencyValue[] 81 | */ 82 | public function getCurrencies() : array 83 | { 84 | return $this->currencies; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /data/fixtures/accountinfo.response.json: -------------------------------------------------------------------------------- 1 | { 2 | "userAccountInfo": { 3 | "personas": [ 4 | { 5 | "personaId": 6540058052, 6 | "personaName": "PersonaName", 7 | "returningUser": 1, 8 | "onlineAccess": true, 9 | "trial": false, 10 | "userState": null, 11 | "userClubList": [ 12 | { 13 | "year": "2019", 14 | "assetId": 160, 15 | "teamId": 160, 16 | "lastAccessTime": 1569537415, 17 | "platform": "ps3", 18 | "clubName": "clubName1", 19 | "clubAbbr": "clubAbbr1", 20 | "established": 1546179266, 21 | "divisionOnline": 1, 22 | "badgeId": 6000160, 23 | "skuAccessList": { 24 | "FFA19PS4": 1569537654 25 | } 26 | }, 27 | { 28 | "year": "2020", 29 | "assetId": 114029, 30 | "teamId": 114029, 31 | "lastAccessTime": 1572615098, 32 | "platform": "ps3", 33 | "clubName": "clubName2", 34 | "clubAbbr": "clubAbbr2", 35 | "established": 1546179266, 36 | "divisionOnline": 1, 37 | "badgeId": 6114029, 38 | "skuAccessList": { 39 | "FFA20PS4": 1572615654 40 | } 41 | }, 42 | { 43 | "year": "2019", 44 | "assetId": 160, 45 | "teamId": 160, 46 | "lastAccessTime": 1566051349, 47 | "platform": "ps3", 48 | "clubName": "clubName3", 49 | "clubAbbr": "clubAbbr2", 50 | "established": 1546179654, 51 | "divisionOnline": 0, 52 | "badgeId": 6000160, 53 | "skuAccessList": null 54 | } 55 | ], 56 | "trialFree": false 57 | } 58 | ] 59 | } 60 | } -------------------------------------------------------------------------------- /src/Authentication/Credentials.php: -------------------------------------------------------------------------------- 1 | email = $email; 46 | $this->password = $password; 47 | $this->locale = $locale; 48 | $this->country = $country; 49 | $this->emulate = $emulate; 50 | 51 | $this->setPlatform($platform); 52 | } 53 | 54 | public function getEmail() : string 55 | { 56 | return $this->email; 57 | } 58 | 59 | public function getPassword() : string 60 | { 61 | return $this->password; 62 | } 63 | 64 | public function getPlatform() : string 65 | { 66 | return $this->platform; 67 | } 68 | 69 | public function getEmulate() : string 70 | { 71 | return $this->emulate; 72 | } 73 | 74 | public function getLocale() : string 75 | { 76 | return $this->locale; 77 | } 78 | 79 | public function getCountry() : string 80 | { 81 | return $this->country; 82 | } 83 | 84 | protected function setPlatform(string $platform) : void 85 | { 86 | if (! in_array($platform, self::VALID_PLATFORMS, true)) { 87 | throw new FutException('Wrong platform. (Valid ones are pc/xbox/xbox360/ps3/ps4)', [ 88 | 'reason' => 'invalid_platform', 89 | ]); 90 | } 91 | 92 | $this->platform = $platform; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Model/Proxy.php: -------------------------------------------------------------------------------- 1 | protocol = $protocol; 32 | $this->ip = $ip; 33 | $this->port = $port; 34 | $this->username = $username; 35 | $this->password = $password; 36 | } 37 | 38 | public function getProtocol() : string 39 | { 40 | return $this->protocol; 41 | } 42 | 43 | public function getIp() : string 44 | { 45 | return $this->ip; 46 | } 47 | 48 | public function setIp(string $ip) : void 49 | { 50 | $this->ip = $ip; 51 | } 52 | 53 | public function getPort() : string 54 | { 55 | return $this->port; 56 | } 57 | 58 | public function setPort(string $port) : void 59 | { 60 | $this->port = $port; 61 | } 62 | 63 | public function getUsername() : ?string 64 | { 65 | return $this->username; 66 | } 67 | 68 | public function setUsername(?string $username) : void 69 | { 70 | $this->username = $username; 71 | } 72 | 73 | public function getPassword() : ?string 74 | { 75 | return $this->password; 76 | } 77 | 78 | public function setPassword(?string $password) : void 79 | { 80 | $this->password = $password; 81 | } 82 | 83 | public function getProxyProtocol() : string 84 | { 85 | $auth = ''; 86 | 87 | if ($this->getUsername() !== null) { 88 | $auth = $this->getUsername(); 89 | 90 | if ($this->getPassword() !== null) { 91 | $auth .= ':' . $this->getPassword(); 92 | } 93 | 94 | $auth .= '@'; 95 | } 96 | 97 | return $this->getProtocol() . '://' . $auth . $this->getIp() . ':' . $this->getPort(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/Api/PinTest.php: -------------------------------------------------------------------------------- 1 | createMock(Session::class); 22 | $session->method('getDob')->willReturn('2019-02-15'); 23 | $session->method('getPersona')->willReturn('persona_stuff'); 24 | $session->method('getNucleus')->willReturn('nucleus_stuff'); 25 | $session->method('getSession')->willReturn('session_stuff'); 26 | 27 | $account = $this->createMock(Account::class); 28 | $account->method('getSession')->willReturn($session); 29 | 30 | $factory = $this->createMock(ClientFactory::class); 31 | 32 | $pin = new Pin($account, $factory); 33 | 34 | $eventLogin = $pin->event('login', 'Page'); 35 | $eventError = $pin->event('error'); 36 | 37 | self::assertEquals([ 38 | 'pgid' => 'Page', 39 | 'type' => 'utas', 40 | 'userid' => 'persona_stuff', 41 | 'core' => [ 42 | 'dob' => '2019-02-15', 43 | 'en' => 'login', 44 | 'pid' => 'persona_stuff', 45 | 'pidm' => [ 46 | 'nucleus' => 'nucleus_stuff', 47 | ], 48 | 'pidt' => 'persona', 49 | 's' => 2, 50 | 'ts_event' => $date->format('Y-m-d\TH:i:s.v\Z'), 51 | ], 52 | ], $eventLogin); 53 | 54 | self::assertEquals([ 55 | 'type' => 'disconnect', 56 | 'core' => [ 57 | 'dob' => '2019-02-15', 58 | 'en' => 'error', 59 | 'pid' => 'persona_stuff', 60 | 'pidm' => [ 61 | 'nucleus' => 'nucleus_stuff', 62 | ], 63 | 'pidt' => 'persona', 64 | 's' => 3, 65 | 'ts_event' => $date->format('Y-m-d\TH:i:s.v\Z'), 66 | ], 67 | 'server_type' => 'utas', 68 | 'errid' => 'server_error', 69 | 'sid' => 'session_stuff', 70 | ], $eventError); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Items/TradeItem.php: -------------------------------------------------------------------------------- 1 | item = $item; 23 | } 24 | 25 | public function getTradeId() : int 26 | { 27 | return $this->get('tradeId'); 28 | } 29 | 30 | public function getItem() : ItemInterface 31 | { 32 | return $this->item; 33 | } 34 | 35 | public function getTradeState() : ?string 36 | { 37 | return $this->get('tradeState'); 38 | } 39 | 40 | public function getBuyNowPrice() : int 41 | { 42 | return $this->get('buyNowPrice'); 43 | } 44 | 45 | public function getBidValue() : int 46 | { 47 | return $this->getCurrentBid() > 200 ? $this->getCurrentBid() : $this->getStartingBid(); 48 | } 49 | 50 | public function getCurrentBid() : int 51 | { 52 | return $this->get('currentBid'); 53 | } 54 | 55 | public function getOffers() : int 56 | { 57 | return $this->get('offers'); 58 | } 59 | 60 | public function isWatched() : bool 61 | { 62 | return $this->get('watched'); 63 | } 64 | 65 | public function getBidState() : ?string 66 | { 67 | return $this->get('bidState'); 68 | } 69 | 70 | public function getStartingBid() : int 71 | { 72 | return $this->get('startingBid'); 73 | } 74 | 75 | public function getConfidenceValue() : int 76 | { 77 | return $this->get('confidenceValue'); 78 | } 79 | 80 | public function getExpires() : int 81 | { 82 | return $this->get('expires'); 83 | } 84 | 85 | public function getExpireDate() : ?DateTime 86 | { 87 | if ($this->getExpires() > 0) { 88 | return new Carbon('+' . $this->getExpires() . ' seconds'); 89 | } 90 | 91 | return null; 92 | } 93 | 94 | public function getSellerName() : ?string 95 | { 96 | return $this->get('sellerName'); 97 | } 98 | 99 | public function getSellerEstablished() : int 100 | { 101 | return $this->get('sellerEstablished'); 102 | } 103 | 104 | public function getSellerId() : int 105 | { 106 | return $this->get('sellerId'); 107 | } 108 | 109 | public function isTradeOwner() : ?bool 110 | { 111 | return $this->get('tradeOwner'); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /data/fixtures/sets_challenges.response.json: -------------------------------------------------------------------------------- 1 | { 2 | "challenges": [ 3 | { 4 | "name": "Hexenkessel", 5 | "priority": 0, 6 | "status": "NOT_STARTED", 7 | "setId": 100, 8 | "description": "Wähle die richtigen Zutaten, um die Challenge abzuschließen!", 9 | "challengeId": 212, 10 | "endTime": 15376, 11 | "repeatable": false, 12 | "formation": "f4411", 13 | "timesCompleted": 0, 14 | "elgReq": [ 15 | { 16 | "type": "SAME_NATION_COUNT", 17 | "eligibilitySlot": 1, 18 | "eligibilityKey": 4, 19 | "eligibilityValue": 3 20 | }, 21 | { 22 | "type": "SCOPE", 23 | "eligibilitySlot": 1, 24 | "eligibilityKey": 13, 25 | "eligibilityValue": 1 26 | }, 27 | { 28 | "type": "SAME_LEAGUE_COUNT", 29 | "eligibilitySlot": 2, 30 | "eligibilityKey": 5, 31 | "eligibilityValue": 4 32 | }, 33 | { 34 | "type": "SCOPE", 35 | "eligibilitySlot": 2, 36 | "eligibilityKey": 13, 37 | "eligibilityValue": 1 38 | }, 39 | { 40 | "type": "PLAYER_COUNT", 41 | "eligibilitySlot": 3, 42 | "eligibilityKey": 2, 43 | "eligibilityValue": 3 44 | }, 45 | { 46 | "type": "SCOPE", 47 | "eligibilitySlot": 3, 48 | "eligibilityKey": 13, 49 | "eligibilityValue": 2 50 | }, 51 | { 52 | "type": "PLAYER_LEVEL", 53 | "eligibilitySlot": 3, 54 | "eligibilityKey": 17, 55 | "eligibilityValue": 3 56 | }, 57 | { 58 | "type": "TEAM_CHEMISTRY", 59 | "eligibilitySlot": 4, 60 | "eligibilityKey": 1, 61 | "eligibilityValue": 60 62 | }, 63 | { 64 | "type": "SCOPE", 65 | "eligibilitySlot": 4, 66 | "eligibilityKey": 13, 67 | "eligibilityValue": 0 68 | } 69 | ], 70 | "elgOperation": "AND", 71 | "tutorial": 0, 72 | "type": "BRICK_CHALLENGE", 73 | "challengeImageId": "100006-2dcb22ad-3800" 74 | } 75 | ] 76 | } -------------------------------------------------------------------------------- /src/Authentication/Session.php: -------------------------------------------------------------------------------- 1 | persona = $persona; 46 | $this->nucleus = $nucleus; 47 | $this->phishing = $phishing; 48 | $this->session = $session; 49 | $this->dob = $dob; 50 | $this->accessToken = $accessToken; 51 | $this->tokenType = $tokenType; 52 | $this->expiresAt = $expiresAt; 53 | } 54 | 55 | public static function create( 56 | string $persona, 57 | string $nucleus, 58 | string $phishing, 59 | string $session, 60 | string $dob, 61 | string $accessToken, 62 | ?string $tokenType = null, 63 | ?DateTime $expiresAt = null 64 | ) : Session { 65 | return new self($persona, $nucleus, $phishing, $session, $dob, $accessToken, $tokenType, $expiresAt); 66 | } 67 | 68 | public function getPersona() : string 69 | { 70 | return $this->persona; 71 | } 72 | 73 | public function getNucleus() : string 74 | { 75 | return $this->nucleus; 76 | } 77 | 78 | public function getPhishing() : string 79 | { 80 | return $this->phishing; 81 | } 82 | 83 | public function getSession() : string 84 | { 85 | return $this->session; 86 | } 87 | 88 | public function getDob() : string 89 | { 90 | return $this->dob; 91 | } 92 | 93 | public function getAccessToken() : string 94 | { 95 | return $this->accessToken; 96 | } 97 | 98 | public function getTokenType() : ?string 99 | { 100 | return $this->tokenType; 101 | } 102 | 103 | public function getExpiresAt() : ?DateTime 104 | { 105 | return $this->expiresAt; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/Api/Endpoint/EndpointTestCase.php: -------------------------------------------------------------------------------- 1 | createMock(Session::class); 30 | $session->method('getDob')->willReturn('2019-02-15'); 31 | $session->method('getPersona')->willReturn('persona_stuff'); 32 | $session->method('getNucleus')->willReturn('nucleus_stuff'); 33 | $session->method('getSession')->willReturn('session_stuff'); 34 | 35 | $credentials = $this->createMock(Credentials::class); 36 | $credentials->method('getPlatform')->willReturn('ps4'); 37 | 38 | $account = $this->createMock(Account::class); 39 | $account->method('getSession')->willReturn($session); 40 | $account->method('getCredentials')->willReturn($credentials); 41 | 42 | $pin = $this->createMock(Pin::class); 43 | 44 | $config = new Config(); 45 | 46 | $core = new Core($account, $config, $factory); 47 | 48 | $reflectionClass = new ReflectionClass($core); 49 | 50 | $reflectionProperty = $reflectionClass->getProperty('pin'); 51 | $reflectionProperty->setAccessible(true); 52 | $reflectionProperty->setValue($core, $pin); 53 | 54 | return $core; 55 | } 56 | 57 | protected function createClientFactoryMock(string $filename) : ClientFactory 58 | { 59 | $jsonData = file_get_contents(__DIR__ . '/../../../data/fixtures/' . $filename); 60 | 61 | $body = $this->createMock(StreamInterface::class); 62 | $body->method('getContents')->willReturn($jsonData); 63 | 64 | $response = $this->createMock(ResponseInterface::class); 65 | $response->method('getBody')->willReturn($body); 66 | $response->method('getStatusCode')->willReturn(200); 67 | 68 | $clientCall = $this->createMock(ClientCall::class); 69 | $clientCall->method('getResponse')->willReturn($response); 70 | $clientCall->method('getContent')->willReturn($jsonData); 71 | 72 | $factory = $this->createMock(ClientFactory::class); 73 | $factory->method('request')->willReturn($clientCall); 74 | 75 | return $factory; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Config/Config.php: -------------------------------------------------------------------------------- 1 | futConfigUrl = $futConfigUrl; 37 | $this->resolveOptions($options); 38 | } 39 | 40 | public function isDelay() : bool 41 | { 42 | return $this->getOption('delay'); 43 | } 44 | 45 | public function getDelayMinTime() : int 46 | { 47 | return $this->getOption('delayMinTime'); 48 | } 49 | 50 | public function getDelayMaxTime() : int 51 | { 52 | return $this->getOption('delayMaxTime'); 53 | } 54 | 55 | public function getRandomDelayTime(?int $min = null, ?int $max = null) : int 56 | { 57 | if ($min === null) { 58 | $min = $this->getDelayMinTime(); 59 | } 60 | 61 | if ($max === null) { 62 | $max = $this->getDelayMaxTime(); 63 | } 64 | 65 | $delayMS = random_int($min, $max); 66 | 67 | return $delayMS * 1000; 68 | } 69 | 70 | public function getUserAgent() : string 71 | { 72 | return $this->getOption('userAgent'); 73 | } 74 | 75 | /** 76 | * @inheritDoc 77 | */ 78 | public function getOptions() : array 79 | { 80 | return $this->options; 81 | } 82 | 83 | /** 84 | * @inheritDoc 85 | */ 86 | public function getOption(string $name) 87 | { 88 | return $this->options[$name]; 89 | } 90 | 91 | /** 92 | * @inheritDoc 93 | */ 94 | public function setOption(string $name, $value) : void 95 | { 96 | $this->options[$name] = $value; 97 | } 98 | 99 | protected function getResolver() : OptionsResolver 100 | { 101 | $resolver = new OptionsResolver(); 102 | 103 | $url = $this->futConfigUrl; 104 | $content = file_get_contents($url); 105 | 106 | $futConfig = $content !== false? json_decode($content, true, 512, JSON_THROW_ON_ERROR) :null; 107 | 108 | if (! is_array($futConfig)) { 109 | $futConfig = []; 110 | } 111 | 112 | $defaults = array_merge($futConfig, [ 113 | 'userAgent' => self::USER_AGENT, 114 | 'delay' => true, 115 | 'delayMinTime' => 1000, 116 | 'delayMaxTime' => 1500, 117 | ]); 118 | 119 | $resolver->setDefaults($defaults); 120 | 121 | return $resolver; 122 | } 123 | 124 | /** 125 | * @param mixed[] $options 126 | */ 127 | protected function resolveOptions(array $options) : void 128 | { 129 | $this->options = $this->getResolver()->resolve($options); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shapecode/fut-api", 3 | "description": "FIFA WebApp API", 4 | "keywords": [ 5 | "fut", 6 | "fifa", 7 | "api", 8 | "fifa20" 9 | ], 10 | "type": "library", 11 | "license": "GPL-3.0-or-later", 12 | "homepage": "https://github.com/shapecode/fut-api", 13 | "support": { 14 | "email": "git@shapecode.de", 15 | "issues": "https://github.com/shapecode/fut-api/issues", 16 | "source": "https://github.com/shapecode/fut-api/releases", 17 | "wiki": "https://github.com/shapecode/fut-api/wiki" 18 | }, 19 | "authors": [ 20 | { 21 | "name": "Shapecode", 22 | "homepage": "https://shapecode.de", 23 | "email": "git@shapecode.de" 24 | } 25 | ], 26 | "require": { 27 | "php": "~7.3", 28 | "ext-json": "*", 29 | "ext-mbstring": "*", 30 | 31 | "symfony/yaml": "^3.4|^4.0", 32 | "symfony/translation": "^3.4|^4.0", 33 | "symfony/stopwatch": "^3.4|^4.0", 34 | "symfony/options-resolver": "^3.4|^4.0", 35 | 36 | "webmozart/assert": "^1.5", 37 | "nesbot/carbon": "^2.25", 38 | "psr/http-factory": "^1.0", 39 | "psr/http-message": "^1.0", 40 | "psr/log": "^1.0", 41 | "php-http/logger-plugin": "^1.0", 42 | "php-http/stopwatch-plugin": "^1.0", 43 | "php-http/discovery": "^1.4", 44 | "php-http/client-common": "^2.0", 45 | "php-http/message": "^1.7", 46 | "php-http/httplug": "^2.0", 47 | "php-http/promise": "^1.0", 48 | "php-http/message-factory": "^1.0", 49 | "php-http/guzzle6-adapter": "^2.0", 50 | "guzzlehttp/psr7": "^1.6", 51 | "guzzlehttp/promises": "^1.3", 52 | "guzzlehttp/guzzle": "^6.4", 53 | "nyholm/psr7": "^1.2" 54 | }, 55 | "require-dev": { 56 | "doctrine/coding-standard": "^6.0", 57 | "roave/security-advisories": "dev-master", 58 | "squizlabs/php_codesniffer": "^3.4", 59 | "phpstan/phpstan": "^0.11.16", 60 | "phpstan/phpstan-deprecation-rules": "^0.11.2", 61 | "phpstan/phpstan-phpunit": "^0.11.2", 62 | "phpstan/phpstan-strict-rules": "^0.11.1", 63 | "maglnet/composer-require-checker": "^2.0", 64 | "phpunit/phpunit": "^8.4", 65 | "symfony/var-dumper": "^4.3", 66 | "symfony/dotenv": "^4.3", 67 | "icanhazstring/composer-unused": "^0.6.2" 68 | }, 69 | "autoload": { 70 | "psr-4": { 71 | "Shapecode\\FUT\\Client\\": "src/" 72 | }, 73 | "files": [ 74 | "src/bootstrap.php" 75 | ] 76 | }, 77 | "autoload-dev": { 78 | "psr-4": { 79 | "Shapecode\\FUT\\Client\\Tests\\": "tests/" 80 | } 81 | }, 82 | "scripts": { 83 | "check": [ 84 | "@crc", 85 | "@cs-fix", 86 | "@cs-check", 87 | "@phpstan", 88 | "@phpunit" 89 | ], 90 | "check-build": [ 91 | "@crc", 92 | "@cs-check", 93 | "@phpstan", 94 | "@phpunit" 95 | ], 96 | "phpstan": "./vendor/bin/phpstan analyse ./src", 97 | "crc": "./vendor/bin/composer-require-checker --config-file=./composer-require-checker.json", 98 | "phpunit": "./vendor/bin/phpunit", 99 | "cs-fix": "./vendor/bin/phpcbf", 100 | "cs-check": "./vendor/bin/phpcs -s" 101 | }, 102 | "extra": { 103 | "branch-alias": { 104 | "dev-master": "20.0-dev" 105 | }, 106 | "unused": [ 107 | "nyholm/psr7" 108 | ] 109 | }, 110 | "minimum-stability": "dev", 111 | "prefer-stable": true 112 | } 113 | -------------------------------------------------------------------------------- /src/Items/Player.php: -------------------------------------------------------------------------------- 1 | get('formation'); 12 | } 13 | 14 | public function getAssetId() : ?int 15 | { 16 | return $this->get('assetId'); 17 | } 18 | 19 | public function getRating() : ?int 20 | { 21 | return $this->get('rating'); 22 | } 23 | 24 | public function getMorale() : ?int 25 | { 26 | return $this->get('morale'); 27 | } 28 | 29 | public function getFitness() : ?int 30 | { 31 | return $this->get('fitness'); 32 | } 33 | 34 | public function getInjuryType() : ?string 35 | { 36 | return $this->get('injuryType'); 37 | } 38 | 39 | public function getInjuryGames() : ?int 40 | { 41 | return $this->get('injuryGames'); 42 | } 43 | 44 | public function getPreferredPosition() : ?string 45 | { 46 | return $this->get('preferredPosition'); 47 | } 48 | 49 | /** 50 | * @return mixed[] 51 | */ 52 | public function getStatsList() : array 53 | { 54 | return $this->get('statsList') ?? []; 55 | } 56 | 57 | /** 58 | * @return mixed[] 59 | */ 60 | public function getLifetimeStats() : array 61 | { 62 | return $this->get('lifetimeStats') ?? []; 63 | } 64 | 65 | public function getTraining() : ?int 66 | { 67 | return $this->get('training'); 68 | } 69 | 70 | public function getContract() : ?int 71 | { 72 | return $this->get('contract'); 73 | } 74 | 75 | public function getSuspension() : ?int 76 | { 77 | return $this->get('suspension'); 78 | } 79 | 80 | /** 81 | * @return mixed[] 82 | */ 83 | public function getAttributeList() : array 84 | { 85 | return $this->get('attributeList') ?? []; 86 | } 87 | 88 | public function getTeamid() : ?int 89 | { 90 | return $this->get('teamid'); 91 | } 92 | 93 | public function getRareflag() : ?bool 94 | { 95 | return $this->get('rareflag'); 96 | } 97 | 98 | public function getPlayStyle() : ?int 99 | { 100 | return $this->get('playStyle'); 101 | } 102 | 103 | public function getLeagueId() : ?int 104 | { 105 | return $this->get('leagueId'); 106 | } 107 | 108 | public function getAssists() : ?int 109 | { 110 | return $this->get('assists'); 111 | } 112 | 113 | public function getLifetimeAssists() : ?int 114 | { 115 | return $this->get('lifetimeAssists'); 116 | } 117 | 118 | public function getLoans() : ?int 119 | { 120 | return $this->get('loans'); 121 | } 122 | 123 | public function getLoyaltyBonus() : ?bool 124 | { 125 | return (bool) $this->get('loyaltyBonus'); 126 | } 127 | 128 | public function getNation() : ?int 129 | { 130 | return $this->get('nation'); 131 | } 132 | 133 | public function getSkillMoves() : ?int 134 | { 135 | return $this->get('skillmoves'); 136 | } 137 | 138 | public function getWeakFootAbilityTypeCode() : ?int 139 | { 140 | return $this->get('weakfootabilitytypecode'); 141 | } 142 | 143 | public function getAttackingWorkRate() : ?int 144 | { 145 | return $this->get('attackingworkrate'); 146 | } 147 | 148 | public function getDefensiveWorkRate() : ?int 149 | { 150 | return $this->get('defensiveworkrate'); 151 | } 152 | 153 | public function getPreferredFoot() : ?int 154 | { 155 | return $this->get('preferredfoot'); 156 | } 157 | 158 | public function getItemType() : string 159 | { 160 | return 'player'; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # FIFA 20 WebApp API 7 | 8 | Manage your FIFA 20 Ultimate Team using this FIFA 20 Ultimate Team API. 9 | Written solely in PHP 10 | 11 | [![paypal](https://img.shields.io/badge/Donate-Paypal-blue.svg)](http://paypal.me/nloges) 12 | 13 | [![PHP Version](https://img.shields.io/packagist/php-v/shapecode/fut-api.svg)](https://packagist.org/packages/shapecode/fut-api) 14 | [![Latest Stable Version](https://img.shields.io/packagist/v/shapecode/fut-api.svg?label=stable)](https://packagist.org/packages/shapecode/fut-api) 15 | [![Latest Unstable Version](https://img.shields.io/packagist/vpre/shapecode/fut-api.svg?label=unstable)](https://packagist.org/packages/shapecode/fut-api) 16 | [![Total Downloads](https://img.shields.io/packagist/dt/shapecode/fut-api.svg)](https://packagist.org/packages/shapecode/fut-api) 17 | [![Monthly Downloads](https://img.shields.io/packagist/dm/shapecode/fut-api.svg?label=monthly)](https://packagist.org/packages/shapecode/fut-api) 18 | [![Daily Downloads](https://img.shields.io/packagist/dd/shapecode/fut-api.svg?label=daily)](https://packagist.org/packages/shapecode/fut-api) 19 | [![License](https://img.shields.io/packagist/l/shapecode/fut-api.svg)](https://packagist.org/packages/shapecode/fut-api) 20 | 21 | 22 | ## Installing 23 | 24 | The recommended way to install FIFA 20 WebApp API is through 25 | [Composer](http://getcomposer.org). 26 | 27 | ```bash 28 | composer require shapecode/fut-api "~20.0@dev" 29 | ``` 30 | 31 | ## Contribute 32 | 33 | Don't be shy. Feel free to contribute and create pull-requests. There's a lot to do. 34 | 35 | ## Usage 36 | 37 | ### Login 38 | 39 | Login parameters: 40 | 41 | - email: [string] email used for logging into the FIFA 20 WebApp 42 | - password: [string] password used for logging into the FIFA 20 WebApp 43 | - platform: [string] pc/ps4/ps4/xbox/xbox360 44 | - code: [string] email code for two-step verification (make sure to use string if your code starts with 0). 45 | 46 | ```php 47 | use FUTApi\Api\Core; 48 | use FUTApi\Exception\FutError; 49 | use FUTApi\Api\Authentication\Account; 50 | use FUTApi\Api\Authentication\Credentials; 51 | use FUTApi\Api\Authentication\Session; 52 | 53 | $credentials = new Credentials($email, $password, $platform); 54 | 55 | // if you already have a valid session 56 | $session = new Session($persona, $nucleus, $phishing, $session, $dob, $accessToken, $tokenType); 57 | 58 | // otherwise 59 | $session = null; 60 | 61 | $account = new Account($credentials, $session); 62 | $fut = new Core($account); 63 | 64 | try { 65 | $login = $fut->login($code); 66 | $session = $account->getSession(); 67 | } catch(FutError $e) { 68 | $reason = $e->getReason(); 69 | die("We have an error logging in: ".$reason); 70 | } 71 | ``` 72 | 73 | After you have initiated your first session, you can then use the API wthout logging in again using the session info. 74 | 75 | 76 | ### Search 77 | 78 | Optional parameters: 79 | 80 | - min_price: [int] Minimal price. 81 | - max_price: [int] Maximum price. 82 | - min_buy: [int] Minimal buy now price. 83 | - max_buy: [int] Maximum buy now price. 84 | - level: ['bronze'/'silver'/gold'] Card level. 85 | - start: [int] Start page number. 86 | - category: ['fitness'/'?'] Card category. 87 | - assetId: [int] assetId. 88 | - defId: [int] defId. 89 | - league: [int] League id. 90 | - club: [int] Club id. 91 | - position: [int?/str?] Position. 92 | - zone: ['attacker'/'?'] zone. 93 | - nationality: [int] Nation id. 94 | - rare: [boolean] True for searching special cards. 95 | - playStyle: [str?] playStyle. 96 | - page_size: [int] Amount of cards on single page (changing this might be risky). 97 | 98 | ```php 99 | $options = []; 100 | $items = $fut->search($options); 101 | ``` 102 | 103 | ### Logout 104 | 105 | Replicates clicking the Logout button. 106 | 107 | ```php 108 | $fut->logout(); 109 | ``` 110 | 111 | 112 | ## License 113 | 114 | GNU GPLv3 115 | 116 | ##### Forked 117 | 118 | https://github.com/InkedCurtis/FUT-API 119 | -------------------------------------------------------------------------------- /data/fixtures/bid.response.json: -------------------------------------------------------------------------------- 1 | { 2 | "credits": 22194, 3 | "auctionInfo": [ 4 | { 5 | "tradeId": 261547766667, 6 | "itemData": { 7 | "id": 369465564985, 8 | "timestamp": 1572616187, 9 | "formation": "f5212", 10 | "untradeable": false, 11 | "assetId": 244791, 12 | "rating": 77, 13 | "itemType": "player", 14 | "resourceId": 244791, 15 | "owners": 3, 16 | "discardValue": 308, 17 | "itemState": "free", 18 | "cardsubtypeid": 2, 19 | "lastSalePrice": 400, 20 | "fitness": 99, 21 | "injuryType": "none", 22 | "injuryGames": 0, 23 | "preferredPosition": "CM", 24 | "training": 0, 25 | "contract": 7, 26 | "teamid": 673, 27 | "rareflag": 0, 28 | "playStyle": 250, 29 | "leagueId": 4, 30 | "assists": 0, 31 | "lifetimeAssists": 0, 32 | "loyaltyBonus": 0, 33 | "pile": 6, 34 | "nation": 43, 35 | "resourceGameYear": 2020, 36 | "attributeArray": [ 37 | 71, 38 | 68, 39 | 76, 40 | 76, 41 | 70, 42 | 68 43 | ], 44 | "statsArray": [ 45 | 0, 46 | 0, 47 | 0, 48 | 0, 49 | 0 50 | ], 51 | "lifetimeStatsArray": [ 52 | 0, 53 | 0, 54 | 0, 55 | 0, 56 | 0 57 | ], 58 | "skillmoves": 2, 59 | "weakfootabilitytypecode": 3, 60 | "attackingworkrate": 2, 61 | "defensiveworkrate": 2, 62 | "trait1": 3072, 63 | "trait2": 0, 64 | "preferredfoot": 1 65 | }, 66 | "tradeState": "closed", 67 | "buyNowPrice": 400, 68 | "currentBid": 400, 69 | "offers": 0, 70 | "watched": false, 71 | "bidState": "buyNow", 72 | "startingBid": 350, 73 | "confidenceValue": 100, 74 | "expires": -1, 75 | "sellerName": "FIFA UT", 76 | "sellerEstablished": 0, 77 | "sellerId": 0, 78 | "tradeOwner": false, 79 | "coinsProcessed": 1, 80 | "tradeIdStr": "261547766667" 81 | } 82 | ], 83 | "bidTokens": {}, 84 | "currencies": [ 85 | { 86 | "name": "COINS", 87 | "funds": 22194, 88 | "finalFunds": 22194 89 | }, 90 | { 91 | "name": "POINTS", 92 | "funds": 0, 93 | "finalFunds": 0 94 | }, 95 | { 96 | "name": "DRAFT_TOKEN", 97 | "funds": 0, 98 | "finalFunds": 0 99 | } 100 | ], 101 | "dynamicObjectivesUpdates": { 102 | "needsGroupsRefresh": true, 103 | "scmpCategoryProgressList": [ 104 | { 105 | "categoryId": 1, 106 | "scmpGroupProgressList": [ 107 | { 108 | "groupId": 136, 109 | "state": 1, 110 | "objectiveProgressList": [ 111 | { 112 | "objectiveId": 22041, 113 | "state": 1, 114 | "progressCount": 29 115 | } 116 | ] 117 | }, 118 | { 119 | "groupId": 235, 120 | "state": 1, 121 | "objectiveProgressList": [ 122 | { 123 | "objectiveId": 22557, 124 | "state": 3, 125 | "progressCount": 1 126 | } 127 | ] 128 | } 129 | ] 130 | }, 131 | { 132 | "categoryId": 1, 133 | "scmpGroupProgressList": [ 134 | { 135 | "groupId": 192, 136 | "state": 1, 137 | "objectiveProgressList": [ 138 | { 139 | "objectiveId": 22385, 140 | "state": 3, 141 | "progressCount": 1 142 | } 143 | ] 144 | } 145 | ] 146 | } 147 | ], 148 | "autoRedeemedXp": 450, 149 | "needsAutoClaim": false 150 | } 151 | } -------------------------------------------------------------------------------- /data/fixtures/watchlist.reponse.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 2, 3 | "credits": 22594, 4 | "auctionInfo": [ 5 | { 6 | "tradeId": 261547235571, 7 | "itemData": { 8 | "id": 366188751336, 9 | "timestamp": 1571143759, 10 | "formation": "f343", 11 | "untradeable": false, 12 | "assetId": 158023, 13 | "rating": 94, 14 | "itemType": "player", 15 | "resourceId": 158023, 16 | "owners": 1, 17 | "discardValue": 752, 18 | "itemState": "forSale", 19 | "cardsubtypeid": 3, 20 | "lastSalePrice": 0, 21 | "fitness": 99, 22 | "injuryType": "none", 23 | "injuryGames": 0, 24 | "preferredPosition": "RW", 25 | "training": 0, 26 | "contract": 18, 27 | "teamid": 241, 28 | "rareflag": 1, 29 | "playStyle": 250, 30 | "leagueId": 53, 31 | "assists": 34, 32 | "lifetimeAssists": 34, 33 | "loyaltyBonus": 1, 34 | "pile": 5, 35 | "nation": 52, 36 | "resourceGameYear": 2020, 37 | "attributeArray": [ 38 | 87, 39 | 92, 40 | 92, 41 | 96, 42 | 39, 43 | 66 44 | ], 45 | "statsArray": [ 46 | 111, 47 | 90, 48 | 1, 49 | 0, 50 | 0 51 | ], 52 | "lifetimeStatsArray": [ 53 | 111, 54 | 90, 55 | 1, 56 | 0, 57 | 0 58 | ], 59 | "skillmoves": 3, 60 | "weakfootabilitytypecode": 4, 61 | "attackingworkrate": 0, 62 | "defensiveworkrate": 1, 63 | "trait1": 58865920, 64 | "trait2": 0, 65 | "preferredfoot": 2 66 | }, 67 | "tradeState": "active", 68 | "buyNowPrice": 1799000, 69 | "currentBid": 0, 70 | "offers": 0, 71 | "watched": true, 72 | "bidState": "none", 73 | "startingBid": 1750000, 74 | "confidenceValue": 100, 75 | "expires": 3004, 76 | "sellerName": "FIFA UT", 77 | "sellerEstablished": 0, 78 | "sellerId": 0, 79 | "tradeOwner": false, 80 | "tradeIdStr": "261547235571" 81 | }, 82 | { 83 | "tradeId": 261372538143, 84 | "itemData": { 85 | "id": 368508640970, 86 | "timestamp": 1571954707, 87 | "formation": "f5221", 88 | "untradeable": false, 89 | "assetId": 202857, 90 | "rating": 82, 91 | "itemType": "player", 92 | "resourceId": 202857, 93 | "owners": 1, 94 | "discardValue": 656, 95 | "itemState": "forSale", 96 | "cardsubtypeid": 3, 97 | "lastSalePrice": 0, 98 | "fitness": 99, 99 | "injuryType": "none", 100 | "injuryGames": 0, 101 | "preferredPosition": "RW", 102 | "training": 0, 103 | "contract": 7, 104 | "teamid": 32, 105 | "rareflag": 1, 106 | "playStyle": 250, 107 | "leagueId": 19, 108 | "assists": 0, 109 | "lifetimeAssists": 0, 110 | "loyaltyBonus": 1, 111 | "pile": 5, 112 | "nation": 21, 113 | "resourceGameYear": 2020, 114 | "attributeArray": [ 115 | 92, 116 | 76, 117 | 74, 118 | 84, 119 | 34, 120 | 71 121 | ], 122 | "statsArray": [ 123 | 0, 124 | 0, 125 | 0, 126 | 0, 127 | 0 128 | ], 129 | "lifetimeStatsArray": [ 130 | 0, 131 | 0, 132 | 0, 133 | 0, 134 | 0 135 | ], 136 | "skillmoves": 3, 137 | "weakfootabilitytypecode": 3, 138 | "attackingworkrate": 2, 139 | "defensiveworkrate": 0, 140 | "trait1": 133632, 141 | "trait2": 0, 142 | "preferredfoot": 1 143 | }, 144 | "tradeState": "active", 145 | "buyNowPrice": 7200, 146 | "currentBid": 750, 147 | "offers": 0, 148 | "watched": true, 149 | "bidState": "highest", 150 | "startingBid": 750, 151 | "confidenceValue": 100, 152 | "expires": 163084, 153 | "sellerName": "FIFA UT", 154 | "sellerEstablished": 0, 155 | "sellerId": 0, 156 | "tradeOwner": false, 157 | "tradeIdStr": "261372538143" 158 | } 159 | ] 160 | } -------------------------------------------------------------------------------- /data/fixtures/tradepile.response.json: -------------------------------------------------------------------------------- 1 | { 2 | "credits": 22594, 3 | "auctionInfo": [ 4 | { 5 | "tradeId": 0, 6 | "itemData": { 7 | "id": 363965480067, 8 | "timestamp": 1570300781, 9 | "formation": "f451", 10 | "untradeable": false, 11 | "assetId": 241497, 12 | "rating": 72, 13 | "itemType": "player", 14 | "resourceId": 241497, 15 | "owners": 1, 16 | "discardValue": 252, 17 | "itemState": "free", 18 | "cardsubtypeid": 1, 19 | "lastSalePrice": 0, 20 | "fitness": 99, 21 | "injuryType": "none", 22 | "injuryGames": 0, 23 | "preferredPosition": "RB", 24 | "training": 0, 25 | "contract": 7, 26 | "teamid": 73, 27 | "rareflag": 1, 28 | "playStyle": 250, 29 | "leagueId": 16, 30 | "assists": 0, 31 | "lifetimeAssists": 0, 32 | "loyaltyBonus": 1, 33 | "pile": 5, 34 | "nation": 18, 35 | "marketDataMinPrice": 300, 36 | "marketDataMaxPrice": 10000, 37 | "resourceGameYear": 2020, 38 | "attributeArray": [ 39 | 83, 40 | 50, 41 | 68, 42 | 77, 43 | 68, 44 | 57 45 | ], 46 | "statsArray": [ 47 | 0, 48 | 0, 49 | 0, 50 | 0, 51 | 0 52 | ], 53 | "lifetimeStatsArray": [ 54 | 0, 55 | 0, 56 | 0, 57 | 0, 58 | 0 59 | ], 60 | "skillmoves": 2, 61 | "weakfootabilitytypecode": 2, 62 | "attackingworkrate": 2, 63 | "defensiveworkrate": 0, 64 | "trait1": 0, 65 | "trait2": 0, 66 | "preferredfoot": 1 67 | }, 68 | "tradeState": null, 69 | "buyNowPrice": 0, 70 | "currentBid": 0, 71 | "offers": 0, 72 | "watched": false, 73 | "bidState": null, 74 | "startingBid": 0, 75 | "confidenceValue": 100, 76 | "expires": 0, 77 | "sellerName": null, 78 | "sellerEstablished": 0, 79 | "sellerId": 0, 80 | "tradeOwner": null, 81 | "tradeIdStr": "0" 82 | }, 83 | { 84 | "tradeId": 0, 85 | "itemData": { 86 | "id": 363256459876, 87 | "timestamp": 1570105892, 88 | "formation": "f5212", 89 | "untradeable": false, 90 | "assetId": 197681, 91 | "rating": 75, 92 | "itemType": "player", 93 | "resourceId": 50529329, 94 | "owners": 1, 95 | "discardValue": 300, 96 | "itemState": "free", 97 | "cardsubtypeid": 2, 98 | "lastSalePrice": 0, 99 | "fitness": 99, 100 | "injuryType": "none", 101 | "injuryGames": 0, 102 | "preferredPosition": "CM", 103 | "training": 0, 104 | "contract": 7, 105 | "teamid": 347, 106 | "rareflag": 0, 107 | "playStyle": 250, 108 | "leagueId": 31, 109 | "assists": 0, 110 | "lifetimeAssists": 0, 111 | "loyaltyBonus": 1, 112 | "pile": 5, 113 | "nation": 18, 114 | "marketDataMinPrice": 300, 115 | "marketDataMaxPrice": 10000, 116 | "resourceGameYear": 2020, 117 | "attributeArray": [ 118 | 68, 119 | 60, 120 | 71, 121 | 78, 122 | 60, 123 | 75 124 | ], 125 | "statsArray": [ 126 | 0, 127 | 0, 128 | 0, 129 | 0, 130 | 0 131 | ], 132 | "lifetimeStatsArray": [ 133 | 0, 134 | 0, 135 | 0, 136 | 0, 137 | 0 138 | ], 139 | "skillmoves": 3, 140 | "weakfootabilitytypecode": 2, 141 | "attackingworkrate": 0, 142 | "defensiveworkrate": 1, 143 | "trait1": 32, 144 | "trait2": 0, 145 | "preferredfoot": 2 146 | }, 147 | "tradeState": null, 148 | "buyNowPrice": 0, 149 | "currentBid": 0, 150 | "offers": 0, 151 | "watched": false, 152 | "bidState": null, 153 | "startingBid": 0, 154 | "confidenceValue": 100, 155 | "expires": 0, 156 | "sellerName": null, 157 | "sellerEstablished": 0, 158 | "sellerId": 0, 159 | "tradeOwner": null, 160 | "tradeIdStr": "0" 161 | } 162 | ], 163 | "bidTokens": {} 164 | } -------------------------------------------------------------------------------- /src/Mapper/Mapper.php: -------------------------------------------------------------------------------- 1 | createTradeItem($a); 37 | } 38 | 39 | return new MarketSearchResponse( 40 | $data, 41 | $auctions, 42 | $bidTokens 43 | ); 44 | } 45 | 46 | /** 47 | * @param mixed[] $data 48 | */ 49 | public function createBidResult(array $data) : BidResponse 50 | { 51 | $as = $data['auctionInfo'] ?? []; 52 | $credits = $data['credits'] ?? null; 53 | 54 | $auctions = []; 55 | 56 | foreach ($as as $a) { 57 | $auctions[] = $this->createTradeItem($a); 58 | } 59 | 60 | return new BidResponse($auctions, $credits); 61 | } 62 | 63 | /** 64 | * @param mixed[] $data 65 | */ 66 | public function createUnassignedResponse(array $data) : UnassignedResponse 67 | { 68 | $itemData = $data['itemData'] ?? []; 69 | $duplicateItemIdList = $data['duplicateItemIdList'] ?? []; 70 | 71 | $items = []; 72 | $duplicates = []; 73 | 74 | foreach ($itemData as $a) { 75 | $items[] = $this->createItem($a); 76 | } 77 | 78 | foreach ($duplicateItemIdList as $a) { 79 | $duplicates[] = $this->createDuplicateItem($a); 80 | } 81 | 82 | return new UnassignedResponse($items, $duplicates); 83 | } 84 | 85 | /** 86 | * @param mixed[] $data 87 | */ 88 | public function createWatchlistResponse(array $data) : WatchlistResponse 89 | { 90 | $credits = $data['credits'] ?? null; 91 | $total = $data['total'] ?? null; 92 | $as = $data['auctionInfo'] ?? []; 93 | 94 | $auctions = []; 95 | 96 | foreach ($as as $a) { 97 | $auctions[] = $this->createTradeItem($a); 98 | } 99 | 100 | return new WatchlistResponse($total, $credits, $auctions); 101 | } 102 | 103 | /** 104 | * @param mixed[] $data 105 | */ 106 | public function createTradepileResponse(array $data) : TradepileResponse 107 | { 108 | $credits = $data['credits'] ?? null; 109 | $as = $data['auctionInfo'] ?? []; 110 | $bs = $search['bidTokens'] ?? []; 111 | 112 | $auctions = []; 113 | $bidTokens = []; 114 | 115 | foreach ($as as $a) { 116 | $auctions[] = $this->createTradeItem($a); 117 | } 118 | 119 | return new TradepileResponse($credits, $auctions, $bidTokens); 120 | } 121 | 122 | /** 123 | * @param mixed[] $data 124 | */ 125 | public function createTradeStatusResponse(array $data) : TradeStatusResponse 126 | { 127 | $credits = $data['credits'] ?? null; 128 | $as = $data['auctionInfo'] ?? []; 129 | $bs = $data['bidTokens'] ?? []; 130 | $cs = $data['currencies'] ?? []; 131 | 132 | $auctions = []; 133 | $bidTokens = []; 134 | $currencies = []; 135 | 136 | foreach ($as as $a) { 137 | $auctions[] = $this->createTradeItem($a); 138 | } 139 | 140 | foreach ($cs as $a) { 141 | $currencies[] = $this->createCurrencyValue($a); 142 | } 143 | 144 | return new TradeStatusResponse($credits, $auctions, $bidTokens, $currencies); 145 | } 146 | 147 | /** 148 | * @param mixed[] $data 149 | */ 150 | public function createTradeItem(array $data) : TradeItem 151 | { 152 | $item = $this->createItem($data['itemData']); 153 | 154 | return new TradeItem($data, $item); 155 | } 156 | 157 | /** 158 | * @param mixed[] $data 159 | */ 160 | public function createCurrencyValue(array $data) : CurrencyValue 161 | { 162 | return new CurrencyValue($data); 163 | } 164 | 165 | /** 166 | * @param mixed[] $data 167 | */ 168 | public function createItem(array $data) : Item 169 | { 170 | $itemType = $data['itemType'] ?? null; 171 | 172 | if ($itemType === 'player') { 173 | return new Player($data); 174 | } 175 | 176 | if ($itemType === 'health') { 177 | return new Health($data); 178 | } 179 | 180 | if ($itemType === 'contract') { 181 | return new Contract($data); 182 | } 183 | 184 | if ($itemType === 'kit') { 185 | return new Kit($data); 186 | } 187 | 188 | return new Item($data); 189 | } 190 | 191 | /** 192 | * @param mixed[] $data 193 | */ 194 | public function createDuplicateItem(array $data) : DuplicateItem 195 | { 196 | return new DuplicateItem($data['itemId'], $data['duplicateItemId']); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Api/Pin.php: -------------------------------------------------------------------------------- 1 | account = $account; 35 | $this->clientFactory = $clientFactory; 36 | } 37 | 38 | public function sendEvent( 39 | string $en, 40 | ?string $pgid = null, 41 | ?string $status = null, 42 | ?string $source = null, 43 | ?string $end_reason = null 44 | ) : void { 45 | $event = $this->event($en, $pgid, $status, $source, $end_reason); 46 | $this->send([$event]); 47 | } 48 | 49 | /** 50 | * @return mixed[] 51 | */ 52 | public function event( 53 | string $en, 54 | ?string $pgid = null, 55 | ?string $status = null, 56 | ?string $source = null, 57 | ?string $end_reason = null 58 | ) : array { 59 | $account = $this->account; 60 | $session = $account->getSession(); 61 | 62 | if ($session === null) { 63 | throw new RuntimeException('session has to be set'); 64 | } 65 | 66 | $data = [ 67 | 'core' => [ 68 | 'dob' => $session->getDob(), 69 | 'en' => $en, 70 | 'pid' => $session->getPersona(), 71 | 'pidm' => [ 72 | 'nucleus' => $session->getNucleus(), 73 | ], 74 | 'pidt' => 'persona', 75 | 's' => $this->s, 76 | // 'didm' => [ 77 | // 'uuid' => '0', 78 | // ], 79 | 'ts_event' => $this->timestamp(), 80 | ], 81 | ]; 82 | 83 | if ($pgid !== null) { 84 | $data['pgid'] = $pgid; 85 | } 86 | 87 | if ($status !== null) { 88 | $data['status'] = $status; 89 | } 90 | 91 | if ($source !== null) { 92 | $data['source'] = $source; 93 | } 94 | 95 | if ($end_reason !== null) { 96 | $data['end_reason'] = $end_reason; 97 | } 98 | 99 | switch ($en) { 100 | case 'login': 101 | $data['type'] = 'utas'; 102 | $data['userid'] = $session->getPersona(); 103 | break; 104 | case 'page_view': 105 | $data['type'] = 'menu'; 106 | break; 107 | case 'error': 108 | $data['server_type'] = 'utas'; 109 | $data['errid'] = 'server_error'; 110 | $data['type'] = 'disconnect'; 111 | $data['sid'] = $session->getSession(); 112 | break; 113 | } 114 | 115 | $this->s++; 116 | 117 | return $data; 118 | } 119 | 120 | /** 121 | * @param mixed[] $events 122 | */ 123 | public function send(array $events) : bool 124 | { 125 | $account = $this->account; 126 | $session = $account->getSession(); 127 | $platform = $account->getCredentials()->getPlatform(); 128 | 129 | if ($session === null) { 130 | throw new RuntimeException('session has to be set'); 131 | } 132 | 133 | $body = json_encode([ 134 | 'custom' => [ 135 | 'networkAccess' => 'G', 136 | 'service_plat' => substr($platform, 0, 3), 137 | ], 138 | 'et' => 'client', 139 | 'events' => $events, 140 | 'gid' => 0, 141 | 'is_sess' => true, 142 | 'loc' => 'en_US', 143 | 'plat' => 'web', 144 | 'rel' => 'prod', 145 | 'sid' => $session->getSession(), 146 | 'taxv' => '1.1', 147 | 'tid' => 'FUT20WEB', 148 | 'tidt' => 'easku', 149 | 'ts_post' => $this->timestamp(), 150 | 'v' => '20.1.0', 151 | ], JSON_THROW_ON_ERROR); 152 | 153 | $headers = [ 154 | 'Accept' => '*/*', 155 | 'Accept-Encoding' => 'gzip, deflate, br', 156 | 'Accept-Language' => 'en-US,de;q=0.9,en-US;q=0.8,en;q=0.7,lb;q=0.6', 157 | 'Cache-Control' => 'no-cache', 158 | 'Connection' => 'kkeep-alive', 159 | 'Content-Length' => mb_strlen($body), 160 | 'Content-Type' => 'application/json', 161 | 'DNT' => '1', 162 | 'Host' => 'pin-river.data.ea.com', 163 | 'Origin' => 'https://www.easports.com', 164 | 'Pragma' => 'no-cache', 165 | 'Referer' => 'https://www.easports.com/fifa/ultimate-team/web-app/', 166 | 'Sec-Fetch-Mode' => 'cors', 167 | 'Sec-Fetch-Site' => 'cross-site', 168 | 'x-ea-game-id' => 'FUT20WEB', 169 | 'x-ea-game-id-type' => 'easku', 170 | 'x-ea-taxv' => '1.1', 171 | ]; 172 | 173 | $call = $this->clientFactory->request($account, 'POST', self::PIN_URL, [ 174 | 'body' => $body, 175 | 'headers' => $headers, 176 | ]); 177 | 178 | $content = json_decode($call->getContent(), true, 512, JSON_THROW_ON_ERROR); 179 | 180 | if ($content['status'] !== 'ok') { 181 | throw new PinErrorException($call->getResponse()); 182 | } 183 | 184 | return true; 185 | } 186 | 187 | private function timestamp() : string 188 | { 189 | return Carbon::now()->format(self::DATETIME_FORMAT); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Http/ClientFactory.php: -------------------------------------------------------------------------------- 1 | config = $config; 69 | $this->cookieJarBuilder = $cookieJarBuilder ?: new CookieJarBuilder(); 70 | $this->logger = $logger ?: new NullLogger(); 71 | 72 | $this->requestFactory = $requestFactory ?: Psr17FactoryDiscovery::findRequestFactory(); 73 | $this->streamFactory = $streamFactory ?: Psr17FactoryDiscovery::findStreamFactory(); 74 | $this->urlFactory = $urlFactory ?: Psr17FactoryDiscovery::findUrlFactory(); 75 | } 76 | 77 | /** 78 | * @inheritdoc 79 | */ 80 | public function request( 81 | AccountInterface $account, 82 | string $method, 83 | string $url, 84 | array $options = [], 85 | array $plugins = [] 86 | ) : ClientCall { 87 | $headers = []; 88 | 89 | if (isset($options['headers'])) { 90 | /** @var mixed[] $headers */ 91 | $headers = $options['headers']; 92 | unset($options['headers']); 93 | } 94 | 95 | $call = new ClientCall(); 96 | 97 | $plugins[] = new HeaderSetPlugin(CoreInterface::REQUEST_HEADERS); 98 | $plugins[] = new HeaderSetPlugin([ 99 | 'User-Agent' => $this->getConfig()->getUserAgent(), 100 | ]); 101 | 102 | if (count($headers) > 0) { 103 | $plugins[] = new HeaderSetPlugin($headers); 104 | } 105 | 106 | $plugins[] = new ContentLengthPlugin(); 107 | $plugins[] = new LoggerPlugin($this->logger); 108 | $stopwatch = new Stopwatch(); 109 | $plugins[] = new StopwatchPlugin($stopwatch); 110 | $plugins[] = new ClientCallPlugin($call); 111 | $plugins[] = new RedirectPlugin(); 112 | 113 | $guzzle = $this->createAccountClient($account, $options); 114 | $client = $this->createPluginClient($guzzle, $plugins); 115 | 116 | $request = $this->createRequest($method, $url); 117 | 118 | $client->sendRequest($request); 119 | 120 | return $call; 121 | } 122 | 123 | /** 124 | * @inheritdoc 125 | */ 126 | protected function createPluginClient(HttpClient $client, array $plugins = []) : PluginClient 127 | { 128 | return new PluginClient($client, $plugins); 129 | } 130 | 131 | /** 132 | * @inheritdoc 133 | */ 134 | protected function createAccountClient( 135 | AccountInterface $account, 136 | array $options = [] 137 | ) : GuzzleAdapter { 138 | $options['http_errors'] = false; 139 | $options['allow_redirects'] = true; 140 | 141 | if ($account->getProxy() !== null) { 142 | $options['proxy'] = $account->getProxy()->getProxyProtocol(); 143 | } 144 | 145 | $options['cookies'] = $this->cookieJarBuilder->createCookieJar($account); 146 | 147 | $stack = HandlerStack::create(new CurlHandler()); 148 | $stack->push(Middleware::retry($this->createRetryHandler())); 149 | 150 | $options['stack'] = $stack; 151 | $options['timeout'] = 5; 152 | 153 | $guzzle = new Client($options); 154 | 155 | return new GuzzleAdapter($guzzle); 156 | } 157 | 158 | /** 159 | * @inheritdoc 160 | */ 161 | protected function createRequest( 162 | string $method, 163 | string $uri, 164 | ?string $body = null, 165 | array $headers = [] 166 | ) : RequestInterface { 167 | $url = $this->urlFactory->createUri($uri); 168 | $request = $this->requestFactory->createRequest($method, $url); 169 | 170 | if ($body !== null) { 171 | $stream = $this->streamFactory->createStream($body); 172 | $request = $request->withBody($stream); 173 | } 174 | 175 | if (count($headers) > 0) { 176 | foreach ($headers as $name => $header) { 177 | $request = $request->withHeader($name, $header); 178 | } 179 | } 180 | 181 | return $request; 182 | } 183 | 184 | protected function getConfig() : ConfigInterface 185 | { 186 | return $this->config; 187 | } 188 | 189 | protected function createRetryHandler() : Closure 190 | { 191 | return static function ( 192 | $retries, 193 | Psr7Request $request, 194 | ?Psr7Response $response = null, 195 | ?RequestException $exception = null 196 | ) { 197 | return $retries < self::MAX_RETRIES; 198 | }; 199 | } 200 | 201 | protected function isServerError(?Psr7Response $response = null) : bool 202 | { 203 | return $response !== null && $response->getStatusCode() >= 500; 204 | } 205 | 206 | protected function isConnectError(?RequestException $exception = null) : bool 207 | { 208 | return $exception instanceof ConnectException; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/Api/CoreInterface.php: -------------------------------------------------------------------------------- 1 | 'utas.external.s2.fut.ea.com:443', 16 | 'ps3' => 'utas.external.s2.fut.ea.com:443', 17 | 'ps4' => 'utas.external.s2.fut.ea.com:443', 18 | 'xbox' => 'utas.external.s3.fut.ea.com:443', 19 | ]; 20 | public const REQUEST_HEADERS = [ 21 | 'Accept' => '*/*', 22 | 'Accept-Encoding' => 'gzip, deflate, br', 23 | 'Accept-Language' => 'en-US,de;q=0.9,en-US;q=0.8,en;q=0.7,lb;q=0.6', 24 | 'Cache-Control' => 'no-cache', 25 | 'Connection' => 'keep-alive', 26 | 'DNT' => '1', 27 | 'Origin' => 'https://www.easports.com', 28 | 'Pragma' => 'no-cache', 29 | 'Referer' => 'https://www.easports.com/fifa/ultimate-team/web-app/', 30 | 'Sec-Fetch-Mode' => 'cors', 31 | 'Sec-Fetch-Site' => 'cross-site', 32 | ]; 33 | public const AUTH_URL = 'utas.mob.v4.fut.ea.com:443'; 34 | public const CLIENT_ID = 'FIFA-20-WEBCLIENT'; 35 | public const SKU = 'FUT20WEB'; 36 | 37 | /** 38 | * @return mixed[] 39 | */ 40 | public function login(?string $code = null) : array; 41 | 42 | public function logout() : void; 43 | 44 | /** 45 | * @return mixed 46 | */ 47 | public function searchDefinition(int $assetId, int $start = 0, int $count = 20); 48 | 49 | /** 50 | * @param mixed[] $params 51 | * 52 | * lev: level 53 | * cat: category 54 | * definitionId: 55 | * micr: min bid price 56 | * macr: max bid price 57 | * minb: min bin price 58 | * maxb: max bin price 59 | * leag: league 60 | * team: club 61 | * pos: position 62 | * zone: 63 | * nation: 64 | * rare: rare type 65 | * playStyle: player style 66 | * 67 | * @return mixed 68 | */ 69 | public function search(array $params = [], int $pageSize = 20, int $start = 0); 70 | 71 | /** 72 | * @param mixed $tradeId 73 | * @param mixed $price 74 | * 75 | * @return mixed 76 | */ 77 | public function bid($tradeId, $price); 78 | 79 | /** 80 | * @return mixed 81 | */ 82 | public function usermassInfo(); 83 | 84 | /** 85 | * @return mixed 86 | */ 87 | public function credits(); 88 | 89 | /** 90 | * @param mixed[] $params 91 | * 92 | * @return mixed 93 | * 94 | * level 95 | * defId 96 | * start 97 | * count 98 | * sort 99 | */ 100 | public function club(array $params = []); 101 | 102 | /** 103 | * @param mixed[] $params 104 | * 105 | * @return mixed[] 106 | */ 107 | public function players(array $params = []) : array; 108 | 109 | /** 110 | * @param mixed[] $params 111 | * 112 | * @return mixed[] 113 | */ 114 | public function stadiums(array $params = []) : array; 115 | 116 | /** 117 | * @param mixed[] $params 118 | * 119 | * @return mixed[] 120 | */ 121 | public function kits(array $params = []) : array; 122 | 123 | /** 124 | * @param mixed[] $params 125 | * 126 | * @return mixed[] 127 | */ 128 | public function staffs(array $params = []) : array; 129 | 130 | /** 131 | * @param mixed[] $params 132 | * 133 | * @return mixed[] 134 | */ 135 | public function badges(array $params = []) : array; 136 | 137 | /** 138 | * @param mixed[] $params 139 | * 140 | * @return mixed[] 141 | */ 142 | public function balls(array $params = []) : array; 143 | 144 | /** 145 | * @return mixed 146 | */ 147 | public function clubStaff(); 148 | 149 | /** 150 | * @return mixed 151 | */ 152 | public function clubConsumables(); 153 | 154 | /** 155 | * @return mixed 156 | */ 157 | public function squad(int $squadId = 0); 158 | 159 | /** 160 | * @param mixed $tradeId 161 | * 162 | * @return mixed 163 | */ 164 | public function tradeStatus($tradeId); 165 | 166 | /** 167 | * @return mixed 168 | */ 169 | public function tradepile(); 170 | 171 | /** 172 | * @return mixed 173 | */ 174 | public function watchlist(); 175 | 176 | /** 177 | * @param mixed $tradeId 178 | * 179 | * @return mixed 180 | */ 181 | public function watchlistDelete($tradeId); 182 | 183 | /** 184 | * @return mixed 185 | */ 186 | public function unassigned(); 187 | 188 | /** 189 | * @param mixed $id 190 | * @param mixed $bid 191 | * @param mixed $bin 192 | * 193 | * @return mixed 194 | */ 195 | public function sell($id, $bid, $bin, int $duration = 3600); 196 | 197 | /** 198 | * @param mixed $itemId 199 | * 200 | * @return mixed 201 | */ 202 | public function quickSell($itemId); 203 | 204 | /** 205 | * @param mixed $tradeId 206 | * 207 | * @return mixed 208 | */ 209 | public function removeSold($tradeId); 210 | 211 | /** 212 | * @param mixed $itemId 213 | * 214 | * @return mixed 215 | */ 216 | public function sendToTradepile($itemId); 217 | 218 | /** 219 | * @param mixed $itemId 220 | * 221 | * @return mixed 222 | */ 223 | public function sendToClub($itemId); 224 | 225 | /** 226 | * @param mixed $tradeId 227 | * 228 | * @return mixed 229 | */ 230 | public function sendToWatchList($tradeId); 231 | 232 | /** 233 | * @param mixed $definitionId 234 | * 235 | * @return mixed 236 | */ 237 | public function priceRange($definitionId); 238 | 239 | /** 240 | * @return mixed 241 | */ 242 | public function relist(); 243 | 244 | /** 245 | * @param mixed $itemId 246 | * @param mixed $resourceId 247 | * 248 | * @return mixed 249 | */ 250 | public function applyConsumable($itemId, $resourceId); 251 | 252 | /** 253 | * @return mixed 254 | */ 255 | public function keepalive(); 256 | 257 | /** 258 | * @return mixed 259 | */ 260 | public function pileSize(); 261 | 262 | /** 263 | * @param mixed $packId 264 | * 265 | * @return mixed 266 | */ 267 | public function buyPack($packId, string $currency = 'COINS'); 268 | 269 | /** 270 | * @param mixed $packId 271 | * 272 | * @return mixed 273 | */ 274 | public function openPack($packId); 275 | 276 | /** 277 | * @return mixed 278 | */ 279 | public function squadBuildingSets(); 280 | 281 | /** 282 | * @param mixed $setId 283 | * 284 | * @return mixed 285 | */ 286 | public function squadBuildingChallenges($setId); 287 | 288 | /** 289 | * @return mixed 290 | */ 291 | public function objectives(string $scope = 'all'); 292 | 293 | /** 294 | * @return mixed|string 295 | */ 296 | public function getCaptchaData(); 297 | 298 | public function validateCaptcha(string $token) : ClientCall; 299 | 300 | public function phishingQuestion() : ClientCall; 301 | 302 | /** 303 | * @param mixed $answer 304 | */ 305 | public function phishingValidate($answer) : ClientCall; 306 | } 307 | -------------------------------------------------------------------------------- /src/Util/EAHasher.php: -------------------------------------------------------------------------------- 1 | hash($string); 45 | } 46 | 47 | private function int32(int $val) : int 48 | { 49 | return $val & 0xFFFFFFFF; 50 | } 51 | 52 | private function ff(int $a, int $b, int $c, int $d, int $x, int $s, int $t) : int 53 | { 54 | $a = $this->int32($a); 55 | $b = $this->int32($b); 56 | $c = $this->int32($c); 57 | $d = $this->int32($d); 58 | $x = $this->int32($x); 59 | $s = $this->int32($s); 60 | $t = $this->int32($t); 61 | 62 | return $this->cmn(($b & $c) | ((~$b) & $d), $a, $b, $x, $s, $t); 63 | } 64 | 65 | private function gg(int $a, int $b, int $c, int $d, int $x, int $s, int $t) : int 66 | { 67 | $a = $this->int32($a); 68 | $b = $this->int32($b); 69 | $c = $this->int32($c); 70 | $d = $this->int32($d); 71 | $x = $this->int32($x); 72 | $s = $this->int32($s); 73 | $t = $this->int32($t); 74 | 75 | return $this->cmn(($b & $d) | ($c & (~$d)), $a, $b, $x, $s, $t); 76 | } 77 | 78 | private function hh(int $a, int $b, int $c, int $d, int $x, int $s, int $t) : int 79 | { 80 | $a = $this->int32($a); 81 | $b = $this->int32($b); 82 | $c = $this->int32($c); 83 | $d = $this->int32($d); 84 | $x = $this->int32($x); 85 | $s = $this->int32($s); 86 | $t = $this->int32($t); 87 | 88 | return $this->cmn($b ^ $c ^ $d, $a, $b, $x, $s, $t); 89 | } 90 | 91 | private function ii(int $a, int $b, int $c, int $d, int $x, int $s, int $t) : int 92 | { 93 | $a = $this->int32($a); 94 | $b = $this->int32($b); 95 | $c = $this->int32($c); 96 | $d = $this->int32($d); 97 | $x = $this->int32($x); 98 | $s = $this->int32($s); 99 | $t = $this->int32($t); 100 | 101 | return $this->cmn($c ^ ($b | (~$d)), $a, $b, $x, $s, $t); 102 | } 103 | 104 | private function cmn(int $q, int $a, int $b, int $x, int $s, int $t) : int 105 | { 106 | $q = $this->int32($q); 107 | $b = $this->int32($b); 108 | $x = $this->int32($x); 109 | $s = $this->int32($s); 110 | $t = $this->int32($t); 111 | 112 | return $this->add($this->bitwiseRotate($this->add($this->add($a, $q), $this->add($x, $t)), $s), $b); 113 | } 114 | 115 | private function add(int $x, int $y) : int 116 | { 117 | $x = $this->int32($x); 118 | $y = $this->int32($y); 119 | $lsw = ($x & 0xFFFF) + ($y & 0xFFFF); 120 | $msw = ($x >> 16) + ($y >> 16) + ($lsw >> 16); 121 | 122 | return ($msw << 16) | ($lsw & 0xFFFF); 123 | } 124 | 125 | private function bitwiseRotate(int $x, int $c) : int 126 | { 127 | $x = $this->int32($x); 128 | 129 | return ($x << $c) | $this->uRShift($x, 32 - $c); 130 | } 131 | 132 | private function uRShift(int $number, int $shiftBits) : int 133 | { 134 | $number = $this->int32($number); 135 | $z = hexdec('80000000'); 136 | // if ($z !== null & $number !== null) { 137 | $number = ($number >> 1); 138 | $number &= (~$z); 139 | $number |= 0x40000000; 140 | $number >>= ($shiftBits - 1); 141 | // } else { 142 | // $number = ($number >> $shiftBits); 143 | // } 144 | 145 | return $number; 146 | } 147 | 148 | private function numberToHex(int $number) : string 149 | { 150 | $result = ''; 151 | for ($j = 0; $j <= 3; $j++) { 152 | $result .= static::$HexCharacters[($number >> ($j * 8 + 4)) & 0x0F]; 153 | $result .= static::$HexCharacters[($number >> ($j * 8)) & 0x0F]; 154 | } 155 | 156 | return $result; 157 | } 158 | 159 | /** 160 | * @return int[] 161 | */ 162 | private function chunkInput(string $input) : array 163 | { 164 | $inputLength = strlen($input); 165 | $numberOfBlocks = $inputLength + 8 >> 6 + 1; 166 | $blocks = []; 167 | for ($i = 0; $i < $numberOfBlocks * 16; $i++) { 168 | $blocks[$i] = 0; 169 | } 170 | for ($i = 0; $i < $inputLength; $i++) { 171 | $blocks[$i >> 2] |= ord($input[$i]) << (($i % 4) * 8); 172 | } 173 | $blocks[$inputLength >> 2] |= 0x80 << (($inputLength % 4) * 8); 174 | $blocks[$numberOfBlocks * 16 - 2] = $inputLength * 8; 175 | 176 | return $blocks; 177 | } 178 | 179 | private function hash(string $string) : string 180 | { 181 | $chunks = $this->chunkInput($string); 182 | $count = count($chunks); 183 | $a = 1732584193; 184 | $b = -271733879; 185 | $c = -1732584194; 186 | $d = 271733878; 187 | for ($i = 0; $i < $count; $i += 16) { 188 | $tempA = $a; 189 | $tempB = $b; 190 | $tempC = $c; 191 | $tempD = $d; 192 | $a = $this->ff($a, $b, $c, $d, $chunks[$i + 0], static::$R1Shifts[0], -680876936); 193 | $d = $this->ff($d, $a, $b, $c, $chunks[$i + 1], static::$R1Shifts[1], -389564586); 194 | $c = $this->ff($c, $d, $a, $b, $chunks[$i + 2], static::$R1Shifts[2], 606105819); 195 | $b = $this->ff($b, $c, $d, $a, $chunks[$i + 3], static::$R1Shifts[3], -1044525330); 196 | $a = $this->ff($a, $b, $c, $d, $chunks[$i + 4], static::$R1Shifts[4], -176418897); 197 | $d = $this->ff($d, $a, $b, $c, $chunks[$i + 5], static::$R1Shifts[5], 1200080426); 198 | $c = $this->ff($c, $d, $a, $b, $chunks[$i + 6], static::$R1Shifts[6], -1473231341); 199 | $b = $this->ff($b, $c, $d, $a, $chunks[$i + 7], static::$R1Shifts[7], -45705983); 200 | $a = $this->ff($a, $b, $c, $d, $chunks[$i + 8], static::$R1Shifts[8], 1770035416); 201 | $d = $this->ff($d, $a, $b, $c, $chunks[$i + 9], static::$R1Shifts[9], -1958414417); 202 | $c = $this->ff($c, $d, $a, $b, $chunks[$i + 10], static::$R1Shifts[10], -42063); 203 | $b = $this->ff($b, $c, $d, $a, $chunks[$i + 11], static::$R1Shifts[11], -1990404162); 204 | $a = $this->ff($a, $b, $c, $d, $chunks[$i + 12], static::$R1Shifts[12], 1804603682); 205 | $d = $this->ff($d, $a, $b, $c, $chunks[$i + 13], static::$R1Shifts[13], -40341101); 206 | $c = $this->ff($c, $d, $a, $b, $chunks[$i + 14], static::$R1Shifts[14], -1502002290); 207 | $b = $this->ff($b, $c, $d, $a, $chunks[$i + 15], static::$R1Shifts[15], 1236535329); 208 | $a = $this->gg($a, $b, $c, $d, $chunks[$i + 1], static::$R2Shifts[0], -165796510); 209 | $d = $this->gg($d, $a, $b, $c, $chunks[$i + 6], static::$R2Shifts[1], -1069501632); 210 | $c = $this->gg($c, $d, $a, $b, $chunks[$i + 11], static::$R2Shifts[2], 643717713); 211 | $b = $this->gg($b, $c, $d, $a, $chunks[$i + 0], static::$R2Shifts[3], -373897302); 212 | $a = $this->gg($a, $b, $c, $d, $chunks[$i + 5], static::$R2Shifts[4], -701558691); 213 | $d = $this->gg($d, $a, $b, $c, $chunks[$i + 10], static::$R2Shifts[5], 38016083); 214 | $c = $this->gg($c, $d, $a, $b, $chunks[$i + 15], static::$R2Shifts[6], -660478335); 215 | $b = $this->gg($b, $c, $d, $a, $chunks[$i + 4], static::$R2Shifts[7], -405537848); 216 | $a = $this->gg($a, $b, $c, $d, $chunks[$i + 9], static::$R2Shifts[8], 568446438); 217 | $d = $this->gg($d, $a, $b, $c, $chunks[$i + 14], static::$R2Shifts[9], -1019803690); 218 | $c = $this->gg($c, $d, $a, $b, $chunks[$i + 3], static::$R2Shifts[10], -187363961); 219 | $b = $this->gg($b, $c, $d, $a, $chunks[$i + 8], static::$R2Shifts[11], 1163531501); 220 | $a = $this->gg($a, $b, $c, $d, $chunks[$i + 13], static::$R2Shifts[12], -1444681467); 221 | $d = $this->gg($d, $a, $b, $c, $chunks[$i + 2], static::$R2Shifts[13], -51403784); 222 | $c = $this->gg($c, $d, $a, $b, $chunks[$i + 7], static::$R2Shifts[14], 1735328473); 223 | $b = $this->gg($b, $c, $d, $a, $chunks[$i + 12], static::$R2Shifts[15], -1926607734); 224 | $a = $this->hh($a, $b, $c, $d, $chunks[$i + 5], static::$R3Shifts[0], -378558); 225 | $d = $this->hh($d, $a, $b, $c, $chunks[$i + 8], static::$R3Shifts[1], -2022574463); 226 | //line below uses _r2Shifts[2] where as MD5 would use _r3Shifts[2] 227 | $c = $this->hh($c, $d, $a, $b, $chunks[$i + 11], static::$R2Shifts[2], 1839030562); 228 | $b = $this->hh($b, $c, $d, $a, $chunks[$i + 14], static::$R3Shifts[3], -35309556); 229 | $a = $this->hh($a, $b, $c, $d, $chunks[$i + 1], static::$R3Shifts[4], -1530992060); 230 | $d = $this->hh($d, $a, $b, $c, $chunks[$i + 4], static::$R3Shifts[5], 1272893353); 231 | $c = $this->hh($c, $d, $a, $b, $chunks[$i + 7], static::$R3Shifts[6], -155497632); 232 | $b = $this->hh($b, $c, $d, $a, $chunks[$i + 10], static::$R3Shifts[7], -1094730640); 233 | $a = $this->hh($a, $b, $c, $d, $chunks[$i + 13], static::$R3Shifts[8], 681279174); 234 | $d = $this->hh($d, $a, $b, $c, $chunks[$i + 0], static::$R3Shifts[9], -358537222); 235 | $c = $this->hh($c, $d, $a, $b, $chunks[$i + 3], static::$R3Shifts[10], -722521979); 236 | $b = $this->hh($b, $c, $d, $a, $chunks[$i + 6], static::$R3Shifts[11], 76029189); 237 | $a = $this->hh($a, $b, $c, $d, $chunks[$i + 9], static::$R3Shifts[12], -640364487); 238 | $d = $this->hh($d, $a, $b, $c, $chunks[$i + 12], static::$R3Shifts[13], -421815835); 239 | $c = $this->hh($c, $d, $a, $b, $chunks[$i + 15], static::$R3Shifts[14], 530742520); 240 | $b = $this->hh($b, $c, $d, $a, $chunks[$i + 2], static::$R3Shifts[15], -995338651); 241 | $a = $this->ii($a, $b, $c, $d, $chunks[$i + 0], static::$R4Shifts[0], -198630844); 242 | $d = $this->ii($d, $a, $b, $c, $chunks[$i + 7], static::$R4Shifts[1], 1126891415); 243 | $c = $this->ii($c, $d, $a, $b, $chunks[$i + 14], static::$R4Shifts[2], -1416354905); 244 | $b = $this->ii($b, $c, $d, $a, $chunks[$i + 5], static::$R4Shifts[3], -57434055); 245 | $a = $this->ii($a, $b, $c, $d, $chunks[$i + 12], static::$R4Shifts[4], 1700485571); 246 | $d = $this->ii($d, $a, $b, $c, $chunks[$i + 3], static::$R4Shifts[5], -1894986606); 247 | $c = $this->ii($c, $d, $a, $b, $chunks[$i + 10], static::$R4Shifts[6], -1051523); 248 | $b = $this->ii($b, $c, $d, $a, $chunks[$i + 1], static::$R4Shifts[7], -2054922799); 249 | $a = $this->ii($a, $b, $c, $d, $chunks[$i + 8], static::$R4Shifts[8], 1873313359); 250 | $d = $this->ii($d, $a, $b, $c, $chunks[$i + 15], static::$R4Shifts[9], -30611744); 251 | $c = $this->ii($c, $d, $a, $b, $chunks[$i + 6], static::$R4Shifts[10], -1560198380); 252 | $b = $this->ii($b, $c, $d, $a, $chunks[$i + 13], static::$R4Shifts[11], 1309151649); 253 | $a = $this->ii($a, $b, $c, $d, $chunks[$i + 4], static::$R4Shifts[12], -145523070); 254 | $d = $this->ii($d, $a, $b, $c, $chunks[$i + 11], static::$R4Shifts[13], -1120210379); 255 | $c = $this->ii($c, $d, $a, $b, $chunks[$i + 2], static::$R4Shifts[14], 718787259); 256 | $b = $this->ii($b, $c, $d, $a, $chunks[$i + 9], static::$R4Shifts[15], -343485551); 257 | //This line is doubled for some reason, line below is not in the MD5 version 258 | $b = $this->ii($b, $c, $d, $a, $chunks[$i + 9], static::$R4Shifts[15], -343485551); 259 | $a = $this->add($a, $tempA); 260 | $b = $this->add($b, $tempB); 261 | $c = $this->add($c, $tempC); 262 | $d = $this->add($d, $tempD); 263 | } 264 | 265 | return $this->numberToHex($a) . $this->numberToHex($b) . $this->numberToHex($c) . $this->numberToHex($d); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /data/fixtures/settings.response.json: -------------------------------------------------------------------------------- 1 | { 2 | "configs": [ 3 | { 4 | "value": 1, 5 | "type": "enableSeasonalCampaigns" 6 | }, 7 | { 8 | "value": 0, 9 | "type": "tokenRedemptionEnabled" 10 | }, 11 | { 12 | "value": 1, 13 | "type": "tradingEnabled" 14 | }, 15 | { 16 | "value": 1, 17 | "type": "enableEntitlementsVerificationInPromoMessages" 18 | }, 19 | { 20 | "value": 1, 21 | "type": "newChemistry" 22 | }, 23 | { 24 | "value": 1, 25 | "type": "enableSpectatorMode" 26 | }, 27 | { 28 | "value": 1, 29 | "type": "coinEnabled_JP" 30 | }, 31 | { 32 | "value": 20200101, 33 | "type": "transferFifaPointsFromLastYearEndDate" 34 | }, 35 | { 36 | "value": 5, 37 | "type": "kitSelectCountdownTimerLengthInSeconds" 38 | }, 39 | { 40 | "value": 1, 41 | "type": "coinEnabled" 42 | }, 43 | { 44 | "value": 10, 45 | "type": "onlineEndSeasonDivision" 46 | }, 47 | { 48 | "value": 15, 49 | "type": "deleteOperationTimeoutSec" 50 | }, 51 | { 52 | "value": 20190101, 53 | "type": "transferFifaPointsFromLastYearStartDate" 54 | }, 55 | { 56 | "value": 0, 57 | "type": "friendlySeasonsEnabled" 58 | }, 59 | { 60 | "value": 1, 61 | "type": "storeEnabled_JP" 62 | }, 63 | { 64 | "value": 1, 65 | "type": "enableSinglePlayerDraftMode" 66 | }, 67 | { 68 | "value": 3, 69 | "type": "numEndMatchRetriesAllowed" 70 | }, 71 | { 72 | "value": 0, 73 | "type": "enableBatchedTrades" 74 | }, 75 | { 76 | "value": 1, 77 | "type": "checkServerDbVersion" 78 | }, 79 | { 80 | "value": 1, 81 | "type": "allowGracePeriodForSquadBuildingSets" 82 | }, 83 | { 84 | "value": 5, 85 | "type": "clubCreateThreshold" 86 | }, 87 | { 88 | "value": 1, 89 | "type": "draftEntryPackStoreEnabled" 90 | }, 91 | { 92 | "value": 1, 93 | "type": "immediateRecoveryAttempt" 94 | }, 95 | { 96 | "value": 0, 97 | "type": "userProgressionDWLoggingEnabled" 98 | }, 99 | { 100 | "value": 1, 101 | "type": "enableLiveMessaging" 102 | }, 103 | { 104 | "value": 1, 105 | "type": "mtxEnabled" 106 | }, 107 | { 108 | "value": 300, 109 | "type": "tFAResendIntervalSecs" 110 | }, 111 | { 112 | "value": 1, 113 | "type": "packOddsLowPercentageLocalizationThreshold" 114 | }, 115 | { 116 | "value": 0, 117 | "type": "enableLegacyYearInfoInItemResourceId" 118 | }, 119 | { 120 | "value": 1, 121 | "type": "enablePlayerPicks" 122 | }, 123 | { 124 | "value": 0, 125 | "type": "enableAbuseReporting" 126 | }, 127 | { 128 | "value": 5000, 129 | "type": "deactivateUsersTimeoutMS" 130 | }, 131 | { 132 | "value": 100, 133 | "type": "transferListSize" 134 | }, 135 | { 136 | "value": 0, 137 | "type": "deviceTemporalScoresEnabled" 138 | }, 139 | { 140 | "value": 2, 141 | "type": "immediateRecoveryAttemptDelay" 142 | }, 143 | { 144 | "value": 1, 145 | "type": "processingStateEnabled" 146 | }, 147 | { 148 | "value": 0, 149 | "type": "enableSavingRegionOnSession" 150 | }, 151 | { 152 | "value": 0, 153 | "type": "tFAEnabled" 154 | }, 155 | { 156 | "value": 1, 157 | "type": "enableGrantLoaner" 158 | }, 159 | { 160 | "value": 1, 161 | "type": "pointsPackStoreEnabled" 162 | }, 163 | { 164 | "value": 1, 165 | "type": "cardPackStoreEnabled" 166 | }, 167 | { 168 | "value": 1, 169 | "type": "enableHouseRules" 170 | }, 171 | { 172 | "value": 1, 173 | "type": "maxPTPSOptimization" 174 | }, 175 | { 176 | "value": 840, 177 | "type": "rivalsInitialSkillPointsInPlacement" 178 | }, 179 | { 180 | "value": 0, 181 | "type": "enableSCMPAutoClaimOnFUTHub" 182 | }, 183 | { 184 | "value": 0, 185 | "type": "allowRemoveFifaPointsOnDeactivateUser" 186 | }, 187 | { 188 | "value": 15, 189 | "type": "houseRulesFriendsListPageSize" 190 | }, 191 | { 192 | "value": 1, 193 | "type": "enableLegendPlayers" 194 | }, 195 | { 196 | "value": 50, 197 | "type": "transferTargetListSize" 198 | }, 199 | { 200 | "value": 7200, 201 | "type": "extendGameSessionTimerSec" 202 | }, 203 | { 204 | "value": 1140, 205 | "type": "clientKeepAliveResetTimeoutSec" 206 | }, 207 | { 208 | "value": 0, 209 | "type": "enableEntitlementDraftEntry" 210 | }, 211 | { 212 | "value": 1, 213 | "type": "fifaPointsEnabled" 214 | }, 215 | { 216 | "value": 1, 217 | "type": "tradeProcessConfidenceValueEnabled" 218 | }, 219 | { 220 | "value": 15, 221 | "type": "postOperationTimeoutSec" 222 | }, 223 | { 224 | "value": 1, 225 | "type": "enableDraftTokenAccountCreation" 226 | }, 227 | { 228 | "value": 15, 229 | "type": "houseRulesCouchPlayMaxMiniSquadCategoriesRetrieved" 230 | }, 231 | { 232 | "value": 1, 233 | "type": "totwHistoryEnabled" 234 | }, 235 | { 236 | "value": 50, 237 | "type": "tradeProcessConfidenceValue" 238 | }, 239 | { 240 | "value": 0, 241 | "type": "enablePackOddsCalculatingDueToManualAction" 242 | }, 243 | { 244 | "value": 0, 245 | "type": "enableTifoInGame" 246 | }, 247 | { 248 | "value": 1000, 249 | "type": "championsGlickoRatingMultiplier" 250 | }, 251 | { 252 | "value": 1, 253 | "type": "returningUserRewardsScreenEnabled" 254 | }, 255 | { 256 | "value": 1, 257 | "type": "enableSquadBuildingSetsFeature" 258 | }, 259 | { 260 | "value": 1, 261 | "type": "enableDraftMode" 262 | }, 263 | { 264 | "value": 1, 265 | "type": "TS2Enabled" 266 | }, 267 | { 268 | "value": 0, 269 | "type": "ps3ConsumableTxnB2B" 270 | }, 271 | { 272 | "value": 10, 273 | "type": "auctionSearchResultsCacheSize" 274 | }, 275 | { 276 | "value": 1, 277 | "type": "enableRivals" 278 | }, 279 | { 280 | "value": 217874930400755712, 281 | "type": "auctionExpiryData" 282 | }, 283 | { 284 | "value": 0, 285 | "type": "enableSimMatch" 286 | }, 287 | { 288 | "value": 1, 289 | "type": "dreamSquadEnabled" 290 | }, 291 | { 292 | "value": 0, 293 | "type": "spectatorModeChampionsEventIdOverride" 294 | }, 295 | { 296 | "value": 20160, 297 | "type": "championsScheduleViewPeriodInMinutes" 298 | }, 299 | { 300 | "value": 1, 301 | "type": "enableItemDataRarityGroupsInfo" 302 | }, 303 | { 304 | "value": 20000, 305 | "type": "ps3B2BAuthCodeTimeout" 306 | }, 307 | { 308 | "value": 1, 309 | "type": "seasonTicketStoreEnabled" 310 | }, 311 | { 312 | "value": 0, 313 | "type": "charityPlayerDbID" 314 | }, 315 | { 316 | "value": 1, 317 | "type": "mtxEnabled_JP" 318 | }, 319 | { 320 | "value": 1800, 321 | "type": "rivalsEventEndTimeInSec" 322 | }, 323 | { 324 | "value": 15, 325 | "type": "getOperationTimeoutSec" 326 | }, 327 | { 328 | "value": 1, 329 | "type": "enableFloatPointSquadRating" 330 | }, 331 | { 332 | "value": 240, 333 | "type": "rivalsPlacementRFactor" 334 | }, 335 | { 336 | "value": 0, 337 | "type": "enableBannerInGame" 338 | }, 339 | { 340 | "value": 24, 341 | "type": "gracePeriodWeekendLeagueInHours" 342 | }, 343 | { 344 | "value": 0, 345 | "type": "enableV4CompanionCommerce" 346 | }, 347 | { 348 | "value": 1, 349 | "type": "allowBrokeringSkuToRequestTargetSku" 350 | }, 351 | { 352 | "value": 0, 353 | "type": "enablePackOddsCalculatingDueToCacheflush" 354 | }, 355 | { 356 | "value": 1, 357 | "type": "enableMatchHistoryLogging" 358 | }, 359 | { 360 | "value": 4, 361 | "type": "firstPartyCommerceFlowVersion" 362 | }, 363 | { 364 | "value": 0, 365 | "type": "rewardProviderEnabled" 366 | }, 367 | { 368 | "value": 1, 369 | "type": "enableLeaderboards" 370 | }, 371 | { 372 | "value": 10, 373 | "type": "offlineEndSeasonDivision" 374 | }, 375 | { 376 | "value": 1, 377 | "type": "allowUntradeableForSquadBuildingSets" 378 | }, 379 | { 380 | "value": 1, 381 | "type": "TS2ForBrokeringEnabled" 382 | }, 383 | { 384 | "value": 1, 385 | "type": "pickPackRulesFixEnabled" 386 | }, 387 | { 388 | "value": 0, 389 | "type": "enableRegionalKillSwitchFramework" 390 | }, 391 | { 392 | "value": 1, 393 | "type": "userTemporalScoresEnabled" 394 | }, 395 | { 396 | "value": 0, 397 | "type": "enableUntradeableQueryParameterForSearchClubApi" 398 | }, 399 | { 400 | "value": 1, 401 | "type": "sendSimEvents" 402 | }, 403 | { 404 | "value": 10, 405 | "type": "houseRulesCouchPlayMaxSquadsRetrieved" 406 | }, 407 | { 408 | "value": 0, 409 | "type": "enableLoyaltyBonusForConceptPlayers" 410 | }, 411 | { 412 | "value": 1, 413 | "type": "tournamentQuitEnabled" 414 | }, 415 | { 416 | "value": 1, 417 | "type": "enableShowPackOdds" 418 | }, 419 | { 420 | "value": 0, 421 | "type": "tradeOffersEnabled" 422 | }, 423 | { 424 | "value": 15, 425 | "type": "putOperationTimeoutSec" 426 | }, 427 | { 428 | "value": 0, 429 | "type": "rivalsSkillPointsCapValue" 430 | }, 431 | { 432 | "value": 1, 433 | "type": "loanPlayerPurchaseFeatureEnable" 434 | }, 435 | { 436 | "value": 1209600, 437 | "type": "SecurityScoreResetExternalScoreApplyTillSecs" 438 | }, 439 | { 440 | "value": 0, 441 | "type": "enableSpecificErrorForBannedUserLogin" 442 | }, 443 | { 444 | "value": 1, 445 | "type": "tokenPackStoreEnabled" 446 | }, 447 | { 448 | "value": 30, 449 | "type": "squadSlots" 450 | }, 451 | { 452 | "value": 5, 453 | "type": "scmpMaxNumberOfRequestedGroups" 454 | }, 455 | { 456 | "value": 1, 457 | "type": "enableDefIdSearch" 458 | }, 459 | { 460 | "value": 1, 461 | "type": "fifaPointsEnabled_JP" 462 | }, 463 | { 464 | "value": 1, 465 | "type": "enableFUTChampions" 466 | }, 467 | { 468 | "value": 1, 469 | "type": "tournamentEnabled" 470 | }, 471 | { 472 | "value": 1, 473 | "type": "cardPackStoreEnabled_JP" 474 | }, 475 | { 476 | "value": 0, 477 | "type": "phishingEnabled" 478 | }, 479 | { 480 | "value": 0, 481 | "type": "enableCelebrationInGame" 482 | }, 483 | { 484 | "value": 1, 485 | "type": "enableDynamicObjectives" 486 | }, 487 | { 488 | "value": 60, 489 | "type": "squadBuildingSetsGracePeriodMinutes" 490 | }, 491 | { 492 | "value": 1, 493 | "type": "enablePointsDraftEntry" 494 | }, 495 | { 496 | "value": 1, 497 | "type": "storeEnabled" 498 | } 499 | ] 500 | } -------------------------------------------------------------------------------- /data/fixtures/sets_challenge.response.json: -------------------------------------------------------------------------------- 1 | { 2 | "challengeId": 212, 3 | "playerRequirements": [ 4 | { 5 | "index": 0, 6 | "playerType": "DEFAULT" 7 | }, 8 | { 9 | "index": 1, 10 | "playerType": "BRICK" 11 | }, 12 | { 13 | "index": 2, 14 | "playerType": "DEFAULT" 15 | }, 16 | { 17 | "index": 3, 18 | "playerType": "DEFAULT" 19 | }, 20 | { 21 | "index": 4, 22 | "playerType": "BRICK" 23 | }, 24 | { 25 | "index": 5, 26 | "playerType": "BRICK" 27 | }, 28 | { 29 | "index": 6, 30 | "playerType": "DEFAULT" 31 | }, 32 | { 33 | "index": 7, 34 | "playerType": "DEFAULT" 35 | }, 36 | { 37 | "index": 8, 38 | "playerType": "BRICK" 39 | }, 40 | { 41 | "index": 9, 42 | "playerType": "DEFAULT" 43 | }, 44 | { 45 | "index": 10, 46 | "playerType": "DEFAULT" 47 | } 48 | ], 49 | "squad": { 50 | "id": 1, 51 | "formation": "f4411", 52 | "rating": 0, 53 | "chemistry": 0, 54 | "manager": [ 55 | { 56 | "id": 0, 57 | "timestamp": 0, 58 | "formation": "any", 59 | "untradeable": false, 60 | "assetId": 0, 61 | "rating": 0, 62 | "itemType": "manager", 63 | "resourceId": 0, 64 | "owners": 0, 65 | "discardValue": 0, 66 | "itemState": "invalid", 67 | "cardsubtypeid": 0, 68 | "lastSalePrice": 0, 69 | "morale": 0, 70 | "fitness": 0, 71 | "injuryGames": 0, 72 | "preferredPosition": "any", 73 | "statsList": [], 74 | "lifetimeStats": [], 75 | "training": 0, 76 | "contract": 0, 77 | "suspension": 0, 78 | "attributeList": [], 79 | "teamid": 0, 80 | "rareflag": 0, 81 | "pile": 0, 82 | "nation": 0, 83 | "resourceGameYear": 2020 84 | } 85 | ], 86 | "players": [ 87 | { 88 | "index": 0, 89 | "itemData": { 90 | "id": 0, 91 | "timestamp": 0, 92 | "formation": "any", 93 | "untradeable": false, 94 | "assetId": 0, 95 | "rating": 0, 96 | "itemType": "player", 97 | "resourceId": 0, 98 | "owners": 0, 99 | "discardValue": 0, 100 | "itemState": "invalid", 101 | "cardsubtypeid": 0, 102 | "lastSalePrice": 0, 103 | "morale": 0, 104 | "fitness": 0, 105 | "injuryGames": 0, 106 | "preferredPosition": "any", 107 | "statsList": [], 108 | "lifetimeStats": [], 109 | "training": 0, 110 | "contract": 0, 111 | "suspension": 0, 112 | "attributeList": [], 113 | "teamid": 0, 114 | "rareflag": 0, 115 | "loyaltyBonus": 1, 116 | "pile": 0, 117 | "nation": 0, 118 | "resourceGameYear": 2020 119 | } 120 | }, 121 | { 122 | "index": 1, 123 | "itemData": { 124 | "id": 0, 125 | "timestamp": 0, 126 | "formation": "any", 127 | "untradeable": false, 128 | "assetId": 0, 129 | "rating": 0, 130 | "itemType": "player", 131 | "resourceId": 0, 132 | "owners": 0, 133 | "discardValue": 0, 134 | "itemState": "invalid", 135 | "cardsubtypeid": 0, 136 | "lastSalePrice": 0, 137 | "morale": 0, 138 | "fitness": 0, 139 | "injuryGames": 0, 140 | "preferredPosition": "any", 141 | "statsList": [], 142 | "lifetimeStats": [], 143 | "training": 0, 144 | "contract": 0, 145 | "suspension": 0, 146 | "attributeList": [], 147 | "teamid": 0, 148 | "rareflag": 0, 149 | "loyaltyBonus": 1, 150 | "pile": 0, 151 | "nation": 0, 152 | "resourceGameYear": 2020 153 | } 154 | }, 155 | { 156 | "index": 2, 157 | "itemData": { 158 | "id": 0, 159 | "timestamp": 0, 160 | "formation": "any", 161 | "untradeable": false, 162 | "assetId": 0, 163 | "rating": 0, 164 | "itemType": "player", 165 | "resourceId": 0, 166 | "owners": 0, 167 | "discardValue": 0, 168 | "itemState": "invalid", 169 | "cardsubtypeid": 0, 170 | "lastSalePrice": 0, 171 | "morale": 0, 172 | "fitness": 0, 173 | "injuryGames": 0, 174 | "preferredPosition": "any", 175 | "statsList": [], 176 | "lifetimeStats": [], 177 | "training": 0, 178 | "contract": 0, 179 | "suspension": 0, 180 | "attributeList": [], 181 | "teamid": 0, 182 | "rareflag": 0, 183 | "loyaltyBonus": 1, 184 | "pile": 0, 185 | "nation": 0, 186 | "resourceGameYear": 2020 187 | } 188 | }, 189 | { 190 | "index": 3, 191 | "itemData": { 192 | "id": 0, 193 | "timestamp": 0, 194 | "formation": "any", 195 | "untradeable": false, 196 | "assetId": 0, 197 | "rating": 0, 198 | "itemType": "player", 199 | "resourceId": 0, 200 | "owners": 0, 201 | "discardValue": 0, 202 | "itemState": "invalid", 203 | "cardsubtypeid": 0, 204 | "lastSalePrice": 0, 205 | "morale": 0, 206 | "fitness": 0, 207 | "injuryGames": 0, 208 | "preferredPosition": "any", 209 | "statsList": [], 210 | "lifetimeStats": [], 211 | "training": 0, 212 | "contract": 0, 213 | "suspension": 0, 214 | "attributeList": [], 215 | "teamid": 0, 216 | "rareflag": 0, 217 | "loyaltyBonus": 1, 218 | "pile": 0, 219 | "nation": 0, 220 | "resourceGameYear": 2020 221 | } 222 | }, 223 | { 224 | "index": 4, 225 | "itemData": { 226 | "id": 0, 227 | "timestamp": 0, 228 | "formation": "any", 229 | "untradeable": false, 230 | "assetId": 0, 231 | "rating": 0, 232 | "itemType": "player", 233 | "resourceId": 0, 234 | "owners": 0, 235 | "discardValue": 0, 236 | "itemState": "invalid", 237 | "cardsubtypeid": 0, 238 | "lastSalePrice": 0, 239 | "morale": 0, 240 | "fitness": 0, 241 | "injuryGames": 0, 242 | "preferredPosition": "any", 243 | "statsList": [], 244 | "lifetimeStats": [], 245 | "training": 0, 246 | "contract": 0, 247 | "suspension": 0, 248 | "attributeList": [], 249 | "teamid": 0, 250 | "rareflag": 0, 251 | "loyaltyBonus": 1, 252 | "pile": 0, 253 | "nation": 0, 254 | "resourceGameYear": 2020 255 | } 256 | }, 257 | { 258 | "index": 5, 259 | "itemData": { 260 | "id": 0, 261 | "timestamp": 0, 262 | "formation": "any", 263 | "untradeable": false, 264 | "assetId": 0, 265 | "rating": 0, 266 | "itemType": "player", 267 | "resourceId": 0, 268 | "owners": 0, 269 | "discardValue": 0, 270 | "itemState": "invalid", 271 | "cardsubtypeid": 0, 272 | "lastSalePrice": 0, 273 | "morale": 0, 274 | "fitness": 0, 275 | "injuryGames": 0, 276 | "preferredPosition": "any", 277 | "statsList": [], 278 | "lifetimeStats": [], 279 | "training": 0, 280 | "contract": 0, 281 | "suspension": 0, 282 | "attributeList": [], 283 | "teamid": 0, 284 | "rareflag": 0, 285 | "loyaltyBonus": 1, 286 | "pile": 0, 287 | "nation": 0, 288 | "resourceGameYear": 2020 289 | } 290 | }, 291 | { 292 | "index": 6, 293 | "itemData": { 294 | "id": 0, 295 | "timestamp": 0, 296 | "formation": "any", 297 | "untradeable": false, 298 | "assetId": 0, 299 | "rating": 0, 300 | "itemType": "player", 301 | "resourceId": 0, 302 | "owners": 0, 303 | "discardValue": 0, 304 | "itemState": "invalid", 305 | "cardsubtypeid": 0, 306 | "lastSalePrice": 0, 307 | "morale": 0, 308 | "fitness": 0, 309 | "injuryGames": 0, 310 | "preferredPosition": "any", 311 | "statsList": [], 312 | "lifetimeStats": [], 313 | "training": 0, 314 | "contract": 0, 315 | "suspension": 0, 316 | "attributeList": [], 317 | "teamid": 0, 318 | "rareflag": 0, 319 | "loyaltyBonus": 1, 320 | "pile": 0, 321 | "nation": 0, 322 | "resourceGameYear": 2020 323 | } 324 | }, 325 | { 326 | "index": 7, 327 | "itemData": { 328 | "id": 0, 329 | "timestamp": 0, 330 | "formation": "any", 331 | "untradeable": false, 332 | "assetId": 0, 333 | "rating": 0, 334 | "itemType": "player", 335 | "resourceId": 0, 336 | "owners": 0, 337 | "discardValue": 0, 338 | "itemState": "invalid", 339 | "cardsubtypeid": 0, 340 | "lastSalePrice": 0, 341 | "morale": 0, 342 | "fitness": 0, 343 | "injuryGames": 0, 344 | "preferredPosition": "any", 345 | "statsList": [], 346 | "lifetimeStats": [], 347 | "training": 0, 348 | "contract": 0, 349 | "suspension": 0, 350 | "attributeList": [], 351 | "teamid": 0, 352 | "rareflag": 0, 353 | "loyaltyBonus": 1, 354 | "pile": 0, 355 | "nation": 0, 356 | "resourceGameYear": 2020 357 | } 358 | }, 359 | { 360 | "index": 8, 361 | "itemData": { 362 | "id": 0, 363 | "timestamp": 0, 364 | "formation": "any", 365 | "untradeable": false, 366 | "assetId": 0, 367 | "rating": 0, 368 | "itemType": "player", 369 | "resourceId": 0, 370 | "owners": 0, 371 | "discardValue": 0, 372 | "itemState": "invalid", 373 | "cardsubtypeid": 0, 374 | "lastSalePrice": 0, 375 | "morale": 0, 376 | "fitness": 0, 377 | "injuryGames": 0, 378 | "preferredPosition": "any", 379 | "statsList": [], 380 | "lifetimeStats": [], 381 | "training": 0, 382 | "contract": 0, 383 | "suspension": 0, 384 | "attributeList": [], 385 | "teamid": 0, 386 | "rareflag": 0, 387 | "loyaltyBonus": 1, 388 | "pile": 0, 389 | "nation": 0, 390 | "resourceGameYear": 2020 391 | } 392 | }, 393 | { 394 | "index": 9, 395 | "itemData": { 396 | "id": 0, 397 | "timestamp": 0, 398 | "formation": "any", 399 | "untradeable": false, 400 | "assetId": 0, 401 | "rating": 0, 402 | "itemType": "player", 403 | "resourceId": 0, 404 | "owners": 0, 405 | "discardValue": 0, 406 | "itemState": "invalid", 407 | "cardsubtypeid": 0, 408 | "lastSalePrice": 0, 409 | "morale": 0, 410 | "fitness": 0, 411 | "injuryGames": 0, 412 | "preferredPosition": "any", 413 | "statsList": [], 414 | "lifetimeStats": [], 415 | "training": 0, 416 | "contract": 0, 417 | "suspension": 0, 418 | "attributeList": [], 419 | "teamid": 0, 420 | "rareflag": 0, 421 | "loyaltyBonus": 1, 422 | "pile": 0, 423 | "nation": 0, 424 | "resourceGameYear": 2020 425 | } 426 | }, 427 | { 428 | "index": 10, 429 | "itemData": { 430 | "id": 0, 431 | "timestamp": 0, 432 | "formation": "any", 433 | "untradeable": false, 434 | "assetId": 0, 435 | "rating": 0, 436 | "itemType": "player", 437 | "resourceId": 0, 438 | "owners": 0, 439 | "discardValue": 0, 440 | "itemState": "invalid", 441 | "cardsubtypeid": 0, 442 | "lastSalePrice": 0, 443 | "morale": 0, 444 | "fitness": 0, 445 | "injuryGames": 0, 446 | "preferredPosition": "any", 447 | "statsList": [], 448 | "lifetimeStats": [], 449 | "training": 0, 450 | "contract": 0, 451 | "suspension": 0, 452 | "attributeList": [], 453 | "teamid": 0, 454 | "rareflag": 0, 455 | "loyaltyBonus": 1, 456 | "pile": 0, 457 | "nation": 0, 458 | "resourceGameYear": 2020 459 | } 460 | }, 461 | { 462 | "index": 11, 463 | "itemData": { 464 | "id": 0, 465 | "timestamp": 0, 466 | "formation": "any", 467 | "untradeable": false, 468 | "assetId": 0, 469 | "rating": 0, 470 | "itemType": "player", 471 | "resourceId": 0, 472 | "owners": 0, 473 | "discardValue": 0, 474 | "itemState": "invalid", 475 | "cardsubtypeid": 0, 476 | "lastSalePrice": 0, 477 | "morale": 0, 478 | "fitness": 0, 479 | "injuryGames": 0, 480 | "preferredPosition": "any", 481 | "statsList": [], 482 | "lifetimeStats": [], 483 | "training": 0, 484 | "contract": 0, 485 | "suspension": 0, 486 | "attributeList": [], 487 | "teamid": 0, 488 | "rareflag": 0, 489 | "loyaltyBonus": 1, 490 | "pile": 0, 491 | "nation": 0, 492 | "resourceGameYear": 2020 493 | } 494 | }, 495 | { 496 | "index": 12, 497 | "itemData": { 498 | "id": 0, 499 | "timestamp": 0, 500 | "formation": "any", 501 | "untradeable": false, 502 | "assetId": 0, 503 | "rating": 0, 504 | "itemType": "player", 505 | "resourceId": 0, 506 | "owners": 0, 507 | "discardValue": 0, 508 | "itemState": "invalid", 509 | "cardsubtypeid": 0, 510 | "lastSalePrice": 0, 511 | "morale": 0, 512 | "fitness": 0, 513 | "injuryGames": 0, 514 | "preferredPosition": "any", 515 | "statsList": [], 516 | "lifetimeStats": [], 517 | "training": 0, 518 | "contract": 0, 519 | "suspension": 0, 520 | "attributeList": [], 521 | "teamid": 0, 522 | "rareflag": 0, 523 | "loyaltyBonus": 1, 524 | "pile": 0, 525 | "nation": 0, 526 | "resourceGameYear": 2020 527 | } 528 | }, 529 | { 530 | "index": 13, 531 | "itemData": { 532 | "id": 0, 533 | "timestamp": 0, 534 | "formation": "any", 535 | "untradeable": false, 536 | "assetId": 0, 537 | "rating": 0, 538 | "itemType": "player", 539 | "resourceId": 0, 540 | "owners": 0, 541 | "discardValue": 0, 542 | "itemState": "invalid", 543 | "cardsubtypeid": 0, 544 | "lastSalePrice": 0, 545 | "morale": 0, 546 | "fitness": 0, 547 | "injuryGames": 0, 548 | "preferredPosition": "any", 549 | "statsList": [], 550 | "lifetimeStats": [], 551 | "training": 0, 552 | "contract": 0, 553 | "suspension": 0, 554 | "attributeList": [], 555 | "teamid": 0, 556 | "rareflag": 0, 557 | "loyaltyBonus": 1, 558 | "pile": 0, 559 | "nation": 0, 560 | "resourceGameYear": 2020 561 | } 562 | }, 563 | { 564 | "index": 14, 565 | "itemData": { 566 | "id": 0, 567 | "timestamp": 0, 568 | "formation": "any", 569 | "untradeable": false, 570 | "assetId": 0, 571 | "rating": 0, 572 | "itemType": "player", 573 | "resourceId": 0, 574 | "owners": 0, 575 | "discardValue": 0, 576 | "itemState": "invalid", 577 | "cardsubtypeid": 0, 578 | "lastSalePrice": 0, 579 | "morale": 0, 580 | "fitness": 0, 581 | "injuryGames": 0, 582 | "preferredPosition": "any", 583 | "statsList": [], 584 | "lifetimeStats": [], 585 | "training": 0, 586 | "contract": 0, 587 | "suspension": 0, 588 | "attributeList": [], 589 | "teamid": 0, 590 | "rareflag": 0, 591 | "loyaltyBonus": 1, 592 | "pile": 0, 593 | "nation": 0, 594 | "resourceGameYear": 2020 595 | } 596 | }, 597 | { 598 | "index": 15, 599 | "itemData": { 600 | "id": 0, 601 | "timestamp": 0, 602 | "formation": "any", 603 | "untradeable": false, 604 | "assetId": 0, 605 | "rating": 0, 606 | "itemType": "player", 607 | "resourceId": 0, 608 | "owners": 0, 609 | "discardValue": 0, 610 | "itemState": "invalid", 611 | "cardsubtypeid": 0, 612 | "lastSalePrice": 0, 613 | "morale": 0, 614 | "fitness": 0, 615 | "injuryGames": 0, 616 | "preferredPosition": "any", 617 | "statsList": [], 618 | "lifetimeStats": [], 619 | "training": 0, 620 | "contract": 0, 621 | "suspension": 0, 622 | "attributeList": [], 623 | "teamid": 0, 624 | "rareflag": 0, 625 | "loyaltyBonus": 1, 626 | "pile": 0, 627 | "nation": 0, 628 | "resourceGameYear": 2020 629 | } 630 | }, 631 | { 632 | "index": 16, 633 | "itemData": { 634 | "id": 0, 635 | "timestamp": 0, 636 | "formation": "any", 637 | "untradeable": false, 638 | "assetId": 0, 639 | "rating": 0, 640 | "itemType": "player", 641 | "resourceId": 0, 642 | "owners": 0, 643 | "discardValue": 0, 644 | "itemState": "invalid", 645 | "cardsubtypeid": 0, 646 | "lastSalePrice": 0, 647 | "morale": 0, 648 | "fitness": 0, 649 | "injuryGames": 0, 650 | "preferredPosition": "any", 651 | "statsList": [], 652 | "lifetimeStats": [], 653 | "training": 0, 654 | "contract": 0, 655 | "suspension": 0, 656 | "attributeList": [], 657 | "teamid": 0, 658 | "rareflag": 0, 659 | "loyaltyBonus": 1, 660 | "pile": 0, 661 | "nation": 0, 662 | "resourceGameYear": 2020 663 | } 664 | }, 665 | { 666 | "index": 17, 667 | "itemData": { 668 | "id": 0, 669 | "timestamp": 0, 670 | "formation": "any", 671 | "untradeable": false, 672 | "assetId": 0, 673 | "rating": 0, 674 | "itemType": "player", 675 | "resourceId": 0, 676 | "owners": 0, 677 | "discardValue": 0, 678 | "itemState": "invalid", 679 | "cardsubtypeid": 0, 680 | "lastSalePrice": 0, 681 | "morale": 0, 682 | "fitness": 0, 683 | "injuryGames": 0, 684 | "preferredPosition": "any", 685 | "statsList": [], 686 | "lifetimeStats": [], 687 | "training": 0, 688 | "contract": 0, 689 | "suspension": 0, 690 | "attributeList": [], 691 | "teamid": 0, 692 | "rareflag": 0, 693 | "loyaltyBonus": 1, 694 | "pile": 0, 695 | "nation": 0, 696 | "resourceGameYear": 2020 697 | } 698 | }, 699 | { 700 | "index": 18, 701 | "itemData": { 702 | "id": 0, 703 | "timestamp": 0, 704 | "formation": "any", 705 | "untradeable": false, 706 | "assetId": 0, 707 | "rating": 0, 708 | "itemType": "player", 709 | "resourceId": 0, 710 | "owners": 0, 711 | "discardValue": 0, 712 | "itemState": "invalid", 713 | "cardsubtypeid": 0, 714 | "lastSalePrice": 0, 715 | "morale": 0, 716 | "fitness": 0, 717 | "injuryGames": 0, 718 | "preferredPosition": "any", 719 | "statsList": [], 720 | "lifetimeStats": [], 721 | "training": 0, 722 | "contract": 0, 723 | "suspension": 0, 724 | "attributeList": [], 725 | "teamid": 0, 726 | "rareflag": 0, 727 | "loyaltyBonus": 1, 728 | "pile": 0, 729 | "nation": 0, 730 | "resourceGameYear": 2020 731 | } 732 | }, 733 | { 734 | "index": 19, 735 | "itemData": { 736 | "id": 0, 737 | "timestamp": 0, 738 | "formation": "any", 739 | "untradeable": false, 740 | "assetId": 0, 741 | "rating": 0, 742 | "itemType": "player", 743 | "resourceId": 0, 744 | "owners": 0, 745 | "discardValue": 0, 746 | "itemState": "invalid", 747 | "cardsubtypeid": 0, 748 | "lastSalePrice": 0, 749 | "morale": 0, 750 | "fitness": 0, 751 | "injuryGames": 0, 752 | "preferredPosition": "any", 753 | "statsList": [], 754 | "lifetimeStats": [], 755 | "training": 0, 756 | "contract": 0, 757 | "suspension": 0, 758 | "attributeList": [], 759 | "teamid": 0, 760 | "rareflag": 0, 761 | "loyaltyBonus": 1, 762 | "pile": 0, 763 | "nation": 0, 764 | "resourceGameYear": 2020 765 | } 766 | }, 767 | { 768 | "index": 20, 769 | "itemData": { 770 | "id": 0, 771 | "timestamp": 0, 772 | "formation": "any", 773 | "untradeable": false, 774 | "assetId": 0, 775 | "rating": 0, 776 | "itemType": "player", 777 | "resourceId": 0, 778 | "owners": 0, 779 | "discardValue": 0, 780 | "itemState": "invalid", 781 | "cardsubtypeid": 0, 782 | "lastSalePrice": 0, 783 | "morale": 0, 784 | "fitness": 0, 785 | "injuryGames": 0, 786 | "preferredPosition": "any", 787 | "statsList": [], 788 | "lifetimeStats": [], 789 | "training": 0, 790 | "contract": 0, 791 | "suspension": 0, 792 | "attributeList": [], 793 | "teamid": 0, 794 | "rareflag": 0, 795 | "loyaltyBonus": 1, 796 | "pile": 0, 797 | "nation": 0, 798 | "resourceGameYear": 2020 799 | } 800 | }, 801 | { 802 | "index": 21, 803 | "itemData": { 804 | "id": 0, 805 | "timestamp": 0, 806 | "formation": "any", 807 | "untradeable": false, 808 | "assetId": 0, 809 | "rating": 0, 810 | "itemType": "player", 811 | "resourceId": 0, 812 | "owners": 0, 813 | "discardValue": 0, 814 | "itemState": "invalid", 815 | "cardsubtypeid": 0, 816 | "lastSalePrice": 0, 817 | "morale": 0, 818 | "fitness": 0, 819 | "injuryGames": 0, 820 | "preferredPosition": "any", 821 | "statsList": [], 822 | "lifetimeStats": [], 823 | "training": 0, 824 | "contract": 0, 825 | "suspension": 0, 826 | "attributeList": [], 827 | "teamid": 0, 828 | "rareflag": 0, 829 | "loyaltyBonus": 1, 830 | "pile": 0, 831 | "nation": 0, 832 | "resourceGameYear": 2020 833 | } 834 | }, 835 | { 836 | "index": 22, 837 | "itemData": { 838 | "id": 0, 839 | "timestamp": 0, 840 | "formation": "any", 841 | "untradeable": false, 842 | "assetId": 0, 843 | "rating": 0, 844 | "itemType": "player", 845 | "resourceId": 0, 846 | "owners": 0, 847 | "discardValue": 0, 848 | "itemState": "invalid", 849 | "cardsubtypeid": 0, 850 | "lastSalePrice": 0, 851 | "morale": 0, 852 | "fitness": 0, 853 | "injuryGames": 0, 854 | "preferredPosition": "any", 855 | "statsList": [], 856 | "lifetimeStats": [], 857 | "training": 0, 858 | "contract": 0, 859 | "suspension": 0, 860 | "attributeList": [], 861 | "teamid": 0, 862 | "rareflag": 0, 863 | "loyaltyBonus": 1, 864 | "pile": 0, 865 | "nation": 0, 866 | "resourceGameYear": 2020 867 | } 868 | } 869 | ] 870 | } 871 | } --------------------------------------------------------------------------------