invalidateCacheValue(self::LAST_AUCTION_ID_KEY); $auctionID = ($gameStor->getValue(self::LAST_AUCTION_ID_KEY) ?? 0) + 1; $gameStor->setValue(self::LAST_AUCTION_ID_KEY, $auctionID); return $auctionID; } static public function openAuction(AuctionInfo $info, General $general): ?string { $db = DB::db(); $auctionID = $info->id; $auctionStor = KVStorage::getStorage($db, 'auction'); $openedAuction = $general->getAuxVar('openedAuction') ?? []; $prevAuctionID = $openedAuction[$info->reqResource->value] ?? null; if ($prevAuctionID !== null) { //XXX: 이 구조보다는 차라리 DB에 insert로 넣는 게 나을 수도. $prevAuction = new Auction($prevAuctionID, $general); if (!$prevAuction->info->finished) { return '아직 경매가 끝나지 않았습니다.'; } unset($openedAuction[$info->reqResource->value]); $general->setAuxVar('openedAuction', $openedAuction); } $auctionStor->setValue("id_{$auctionID}", $info->toArray()); } public function getHighestBid(): ?AuctionBidItem { $db = DB::db(); $rawHighestBid = $db->queryFirstRow( 'SELECT * FROM ng_auction WHERE auction_id = %i ORDER BY `amount` DESC LIMIT 1', $this->info->id ); if(!$rawHighestBid){ return null; } return AuctionBidItem::fromArray($rawHighestBid); } public function getMyPrevBid(): ?AuctionBidItem { $db = DB::db(); $rawMyPrevBid = $db->queryFirstRow( 'SELECT * FROM ng_auction WHERE general_id = %i AND auction_id = %i ORDER BY `amount` DESC LIMIT 1', $this->general->getID(), $this->info->id ); if (!$rawMyPrevBid) { return null; } return AuctionBidItem::fromArray($rawMyPrevBid); } public function __construct(protected readonly int $auctionID, protected General $general) { $db = DB::db(); $auctionStor = KVStorage::getStorage($db, 'auction'); $rawAuctionInfo = $auctionStor->getValue("id_{$auctionID}"); if ($rawAuctionInfo === null) { throw new \RuntimeException("해당 경매가 없습니다: {$auctionID}"); } $this->info = AuctionInfo::fromArray($rawAuctionInfo); } public function getInfo(): AuctionInfo { return $this->info; } public function extendCloseDate(DateTimeInterface $date): void { $db = DB::db(); $auctionStor = KVStorage::getStorage($db, 'auction'); $this->info->closeDate = DateTimeImmutable::createFromInterface($date); $auctionStor->setValue("id_{$this->info->id}", $this->info->toArray()); } public function refundBid(AuctionBidItem $bidItem, string $reason): void { if($bidItem->auctionID !== $this->info->id){ throw new \RuntimeException('잘못된 경매입니다.'); } $db = DB::db(); if($bidItem->generalID === $this->general->generalID) { $oldBidder = $this->general; } else { $oldBidder = General::createGeneralObjFromDB($bidItem->general_id); } if($this->info->reqResource === ResourceType::inheritancePoint){ $oldBidder->increaseInheritancePoint(InheritanceKey::previous, $bidItem->amount); } else{ $oldBidder->increaseVar($this->info->reqResource->value, $bidItem->amount); } $staticNation = $oldBidder->getStaticNation(); $src = new MessageTarget(0, '', 0, 'System', '#000000'); $dest = new MessageTarget( $oldBidder->getID(), $oldBidder->getName(), $oldBidder->getNationID(), $staticNation['name'], $staticNation['color'], GetImageURL($oldBidder->getVar('imgsvr'), $oldBidder->getVar('picture')) ); //TODO: 전역 알림이 나타나야한다. 일반 메시지보다는 중요하고, 메시지보단 약하게.. //TODO: 바로가기를 제공하는 편이 좋을 것 같다. $msg = new Message( Message::MSGTYPE_PRIVATE, $src, $dest, $reason, new DateTime(), new DateTime('9999-12-31'), [] ); $oldBidder->applyDB($db); $msg->send(true); } public function closeAuction(bool $isRollback = false): void { $db = DB::db(); $auctionStor = KVStorage::getStorage($db, 'auction'); $this->info->finished = true; $openedAuction = $this->general->getAuxVar('openedAuction') ?? []; if (key_exists($this->info->reqResource->value, $openedAuction)) { unset($openedAuction[$this->info->reqResource->value]); $this->general->setAuxVar('openedAuction', $openedAuction); } if ($isRollback) { $highestBid = $this->getHighestBid(); if($highestBid !== null){ $this->refundBid($highestBid, "{$this->info->title} 경매가 취소되었습니다."); } } $auctionID = $this->info->id; $auctionStor->setValue("id_{$auctionID}", $this->info->toArray()); } private function bidInheritPoint(int $amount, \DateTimeImmutable $now): void { $db = DB::db(); $auctionInfo = $this->info; $general = $this->general; $highestBid = $this->getHighestBid(); if ($highestBid !== null && $amount <= $highestBid->amount) { throw new \RuntimeException('현재입찰가보다 높게 입찰해야 합니다.'); } $rawMyPrevBid = $db->queryFirstRow( 'SELECT * FROM ng_auction WHERE general_id = %i AND auction_id = %i ORDER BY `amount` DESC LIMIT 1', $general->getID(), $auctionInfo->id ); if ($rawMyPrevBid === null) { $myPrevBid = null; } else { $myPrevBid = AuctionBidItem::fromArray($rawMyPrevBid); } $morePoint = $amount - ($myPrevBid ? $myPrevBid->amount : 0); $currPoint = $general->getInheritancePoint(InheritanceKey::previous); if ($currPoint === null || $currPoint < $morePoint) { throw new \RuntimeException('유산포인트가 부족합니다.'); } //여기서부터 입찰 성공 $newBid = new AuctionBidItem( null, $auctionInfo->id, $general->getVar('owner'), $general->getID(), $amount, $now, new AuctionBidItemData( $general->getVar('owner_name'), $general->getName(), ) ); $db->insert('ng_auction', $newBid->toArray()); $general->increaseInheritancePoint(InheritanceKey::previous, -$morePoint); $general->increaseRankVar(RankColumn::inherit_point_spent_dynamic, $morePoint); if ($myPrevBid === null || ($highestBid !== null && $myPrevBid->id !== $highestBid->id)) { $this->refundBid($highestBid, "{$auctionInfo->title} 경매에 상회입찰자가 나타났습니다."); } $general->applyDB($db); } public function bid(int $amount): void { $auctionInfo = $this->info; $general = $this->general; if ($auctionInfo->finished) { throw new \RuntimeException('경매가 이미 끝났습니다.'); } $now = new \DateTimeImmutable(); if ($auctionInfo->closeDate < $now) { throw new \RuntimeException('경매가 이미 끝났습니다.'); } if ($auctionInfo->closeDate > $now) { throw new \RuntimeException('경매가 아직 시작되지 않았습니다.'); } if ($auctionInfo->buyImmediatelyAmount !== null && $auctionInfo->buyImmediatelyAmount < $amount) { throw new \RuntimeException('즉시판매가보다 높을 수 없습니다.'); } if ($auctionInfo->reqResource === ResourceType::inheritancePoint) { $this->bidInheritPoint($amount, $now); return; } //reqResource는 말 그대로 '구매자가 내야하는 자원'이다. $db = DB::db(); $highestBid = $this->getHighestBid(); if ($highestBid !== null && $amount <= $highestBid->amount) { throw new \RuntimeException('현재입찰가보다 높게 입찰해야 합니다.'); } $myPrevBid = $this->getMyPrevBid(); $morePoint = $amount - ($myPrevBid ? $myPrevBid->amount : 0); $resType = $auctionInfo->reqResource; $minReqRes = match ($resType) { ResourceType::gold => GameConst::$defaultGold, ResourceType::rice => GameConst::$defaultRice, }; if ($general->getVar($resType->value) < $morePoint + $minReqRes) { throw new \RuntimeException($resType->getName() . '이 부족합니다.'); } //여기서부터 입찰 성공 $newBid = new AuctionBidItem( null, $auctionInfo->id, $general->getVar('owner'), $general->getID(), $amount, $now, new AuctionBidItemData( $general->getVar('owner_name'), $general->getName(), ) ); $db->insert('ng_auction', $newBid->toArray()); $general->setVar($resType->value, $general->getVar($resType->value) - $morePoint); if ($myPrevBid === null || ($highestBid !== null && $myPrevBid->id !== $highestBid->id)) { $this->refundBid($highestBid, "{$auctionInfo->title} 경매에 상회입찰자가 나타났습니다."); } $general->applyDB($db); } }