package com.github.retro_game.retro_game.service.impl; import com.github.retro_game.retro_game.dto.*; import com.github.retro_game.retro_game.entity.*; import com.github.retro_game.retro_game.model.Item; import com.github.retro_game.retro_game.model.ItemRequirementsUtils; import com.github.retro_game.retro_game.model.ItemTimeUtils; import com.github.retro_game.retro_game.model.unit.UnitItem; import com.github.retro_game.retro_game.repository.BodyRepository; import com.github.retro_game.retro_game.repository.EventRepository; import com.github.retro_game.retro_game.repository.ShipyardQueueEntryRepository; import com.github.retro_game.retro_game.service.exception.*; import io.vavr.Tuple; import io.vavr.Tuple2; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import java.time.Instant; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @Service class ShipyardServiceImpl implements ShipyardServiceInternal { private static final Logger logger = LoggerFactory.getLogger(ShipyardServiceImpl.class); private final ItemTimeUtils itemTimeUtils; private final BodyRepository bodyRepository; private final EventRepository eventRepository; private final ShipyardQueueEntryRepository shipyardQueueEntryRepository; private BodyServiceInternal bodyServiceInternal; private EventScheduler eventScheduler; public ShipyardServiceImpl(ItemTimeUtils itemTimeUtils, BodyRepository bodyRepository, EventRepository eventRepository, ShipyardQueueEntryRepository shipyardQueueEntryRepository) { this.itemTimeUtils = itemTimeUtils; this.bodyRepository = bodyRepository; this.eventRepository = eventRepository; this.shipyardQueueEntryRepository = shipyardQueueEntryRepository; } @Autowired public void setBodyServiceInternal(BodyServiceInternal bodyServiceInternal) { this.bodyServiceInternal = bodyServiceInternal; } @Autowired public void setEventScheduler(EventScheduler eventScheduler) { this.eventScheduler = eventScheduler; } @Override @Transactional(isolation = Isolation.REPEATABLE_READ, readOnly = true) public UnitsAndQueuePairDto getUnitsAndQueuePair(long bodyId, UnitTypeDto type) { Body body = bodyServiceInternal.getUpdated(bodyId); Map<UnitKind, Integer> inQueue = new EnumMap<>(UnitKind.class); List<ShipyardQueueEntry> shipyardQueue = body.getShipyardQueue(); List<ShipyardQueueEntryDto> queue = new ArrayList<>(shipyardQueue.size()); boolean first = true; long finishAt = 0; for (ShipyardQueueEntry entry : shipyardQueue) { UnitKind kind = entry.getKind(); int count = entry.getCount(); assert count >= 1; inQueue.put(kind, inQueue.getOrDefault(kind, 0) + count); var item = Item.get(kind); Resources cost = new Resources(item.getCost()); cost.mul(count); var constructionTime = getConstructionTime(item.getCost(), body); long requiredTime; if (first) { Optional<Event> event = eventRepository.findFirstByKindAndParam(EventKind.SHIPYARD_QUEUE, body.getId()); Assert.isTrue(event.isPresent(), "Event must be present"); long readyAt = event.get().getAt().toInstant().getEpochSecond(); long now = body.getUpdatedAt().toInstant().getEpochSecond(); long remainingTime = readyAt - now; requiredTime = remainingTime + (count - 1) * constructionTime; finishAt = now + requiredTime; first = false; } else { requiredTime = count * constructionTime; finishAt += requiredTime; } queue.add(new ShipyardQueueEntryDto(Converter.convert(kind), count, entry.getSequence(), Converter.convert(cost), Date.from(Instant.ofEpochSecond(finishAt)), requiredTime)); } Map<UnitKind, UnitItem> items; if (type == null) { items = UnitItem.getAll(); } else if (type == UnitTypeDto.DEFENSE) { items = UnitItem.getDefense(); } else { assert type == UnitTypeDto.FLEET; items = UnitItem.getFleet(); } Resources resources = body.getResources(); List<UnitDto> units = new ArrayList<>(items.size()); for (Map.Entry<UnitKind, UnitItem> entry : items.entrySet()) { UnitKind kind = entry.getKey(); UnitItem item = entry.getValue(); int currentCount = body.getUnitsCount(kind); int futureCount = currentCount + inQueue.getOrDefault(kind, 0); var meetsRequirements = ItemRequirementsUtils.meetsRequirements(item, body); if (futureCount > 0 || meetsRequirements) { var time = getConstructionTime(item.getCost(), body); Resources cost = item.getCost(); int maxBuildable = 0; if (meetsRequirements) { maxBuildable = Integer.MAX_VALUE; if (cost.getMetal() > 0.0) { maxBuildable = (int) (resources.getMetal() / cost.getMetal()); } if (cost.getCrystal() > 0.0) { maxBuildable = Math.min(maxBuildable, (int) (resources.getCrystal() / cost.getCrystal())); } if (cost.getDeuterium() > 0.0) { maxBuildable = Math.min(maxBuildable, (int) (resources.getDeuterium() / cost.getDeuterium())); } if (kind == UnitKind.SMALL_SHIELD_DOME || kind == UnitKind.LARGE_SHIELD_DOME) { if (futureCount >= 1) { maxBuildable = 0; } else if (maxBuildable > 1) { maxBuildable = 1; } } else if (kind == UnitKind.ANTI_BALLISTIC_MISSILE || kind == UnitKind.INTERPLANETARY_MISSILE) { int nAnti = body.getUnitsCount(UnitKind.ANTI_BALLISTIC_MISSILE) + inQueue.getOrDefault(UnitKind.ANTI_BALLISTIC_MISSILE, 0); int nInter = body.getUnitsCount(UnitKind.INTERPLANETARY_MISSILE) + inQueue.getOrDefault(UnitKind.INTERPLANETARY_MISSILE, 0); int max = 10 * body.getBuildingLevel(BuildingKind.MISSILE_SILO) - (nAnti + 2 * nInter); if (kind == UnitKind.INTERPLANETARY_MISSILE) { max /= 2; } maxBuildable = Math.min(maxBuildable, max); } } units.add(new UnitDto(Converter.convert(kind), currentCount, futureCount, Converter.convert(cost), time, maxBuildable)); } } // Keep the order defined in the service layer. units.sort(Comparator.comparing(UnitDto::getKind)); return new UnitsAndQueuePairDto(units, queue); } @Override @Transactional(readOnly = true) public Map<UnitKind, Tuple2<Integer, Integer>> getCurrentAndFutureCounts(Body body) { EnumMap<UnitKind, Integer> inQueue = body.getShipyardQueue().stream() .collect(Collectors.toMap( ShipyardQueueEntry::getKind, ShipyardQueueEntry::getCount, (a, b) -> a + b, () -> new EnumMap<>(UnitKind.class) )); return Arrays.stream(UnitKind.values()) .filter(kind -> body.getUnitsCount(kind) != 0 || inQueue.getOrDefault(kind, 0) != 0) .collect(Collectors.toMap( Function.identity(), kind -> { int n = body.getUnitsCount(kind); return Tuple.of(n, n + inQueue.getOrDefault(kind, 0)); }, (a, b) -> { throw new IllegalStateException(); }, () -> new EnumMap<>(UnitKind.class) )); } @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public void build(long bodyId, UnitKindDto kind, int count) { UnitKind k = Converter.convert(kind); Body body = bodyServiceInternal.getUpdated(bodyId); var item = Item.get(k); if (!ItemRequirementsUtils.meetsRequirements(item, body)) { logger.warn("Constructing unit failed, requirements not met: bodyId={} kind={} count={}", bodyId, k, count); throw new RequirementsNotMetException(); } Resources cost = item.getCost(); cost.mul(count); if (!body.getResources().greaterOrEqual(cost)) { logger.warn("Constructing unit failed, not enough resources: bodyId={} kind={} count={}", bodyId, k, count); throw new NotEnoughResourcesException(); } body.getResources().sub(cost); List<ShipyardQueueEntry> queue = body.getShipyardQueue(); if (k == UnitKind.SMALL_SHIELD_DOME || k == UnitKind.LARGE_SHIELD_DOME) { // Special case for shield domes. if (count > 1) { logger.warn("Constructing unit failed, request to build more than one shield dome: bodyId={} kind={} count={}", bodyId, k, count); throw new TooManyShieldDomesException(); } if (body.getUnitsCount(k) >= 1) { logger.warn("Constructing unit failed, the shield dome is already built: bodyId={} kind={} count={}", bodyId, k, count); throw new ShieldDomeAlreadyBuiltException(); } boolean exists = queue.stream().anyMatch(e -> e.getKind() == k); if (exists) { logger.warn("Constructing unit failed, the shield dome is already in the queue: bodyId={} kind={} count={}", bodyId, k, count); throw new ShieldDomeAlreadyInQueueException(); } } else if (k == UnitKind.ANTI_BALLISTIC_MISSILE || k == UnitKind.INTERPLANETARY_MISSILE) { // Special case for missiles, check the capacity. int used = body.getUnitsCount(UnitKind.ANTI_BALLISTIC_MISSILE) + 2 * body.getUnitsCount(UnitKind.INTERPLANETARY_MISSILE); used += queue.stream() .mapToInt(e -> { switch (e.getKind()) { case ANTI_BALLISTIC_MISSILE: return 1; case INTERPLANETARY_MISSILE: return 2; default: return 0; } }) .sum(); used += (k == UnitKind.ANTI_BALLISTIC_MISSILE ? 1 : 2) * count; int cap = 10 * body.getBuildingLevel(BuildingKind.MISSILE_SILO); if (used > cap) { logger.warn("Constructing unit failed, not enough capacity in missile silo: bodyId={} kind={} count={}", bodyId, k, count); throw new NotEnoughCapacityException(); } } logger.info("Constructing unit successful: bodyId={} kind={} count={}", bodyId, k, count); ShipyardQueueEntry last = null; if (!queue.isEmpty()) { last = queue.get(queue.size() - 1); } // Update or add an entry. ShipyardQueueEntry entry; if (last != null && last.getKind() == k) { // Add units to last entry. entry = last; entry.setCount(entry.getCount() + count); } else { ShipyardQueueEntryKey key = new ShipyardQueueEntryKey(); key.setBody(body); key.setSequence(last == null ? 1 : last.getSequence() + 1); entry = new ShipyardQueueEntry(); entry.setKey(key); entry.setKind(k); entry.setCount(count); shipyardQueueEntryRepository.save(entry); } if (last == null) { // Add event. long time = getConstructionTime(item.getCost(), body); Date now = body.getUpdatedAt(); Date startAt = Date.from(Instant.ofEpochSecond(now.toInstant().getEpochSecond() + time)); Event event = new Event(); event.setAt(startAt); event.setKind(EventKind.SHIPYARD_QUEUE); event.setParam(body.getId()); eventScheduler.schedule(event); } } @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public void handle(Event event) { long bodyId = event.getParam(); Body body = bodyRepository.getOne(bodyId); Date at = event.getAt(); eventRepository.delete(event); List<ShipyardQueueEntry> queue = body.getShipyardQueue(); // This shouldn't happen. if (queue.isEmpty()) { logger.error("Handling shipyard queue, queue is empty: bodyId={}", bodyId); return; } // Update resources, as the production may increase when we build solar satellites. bodyServiceInternal.updateResources(body, at); boolean first = true; for (ShipyardQueueEntry entry : queue) { UnitKind kind = entry.getKind(); var item = Item.get(kind); long time = getConstructionTime(item.getCost(), body); int numBuilt; if (time == 0) { // All units can be built right now. numBuilt = entry.getCount(); } else if (first) { // We have waited for that unit. numBuilt = 1; } else { // Next iteration, we haven't waited yet, we must add an event first. numBuilt = 0; } first = false; if (numBuilt > 0) { // Add units. var oldCount = body.getUnitsCount(kind); var newCount = oldCount + numBuilt; body.setUnitsCount(kind, newCount); int count = entry.getCount() - numBuilt; assert count >= 0; if (count == 0) { shipyardQueueEntryRepository.delete(entry); continue; } entry.setCount(count); } // Add event. Date startAt = Date.from(Instant.ofEpochSecond(at.toInstant().getEpochSecond() + time)); Event newEvent = new Event(); newEvent.setAt(startAt); newEvent.setKind(EventKind.SHIPYARD_QUEUE); newEvent.setParam(bodyId); eventScheduler.schedule(newEvent); break; } } @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public void deleteUnitsAndQueue(Body body) { Optional<Event> event = eventRepository.findFirstByKindAndParam(EventKind.SHIPYARD_QUEUE, body.getId()); event.ifPresent(eventRepository::delete); shipyardQueueEntryRepository.deleteAll(body.getShipyardQueue()); } private long getConstructionTime(Resources cost, Body body) { int shipyardLevel = body.getBuildingLevel(BuildingKind.SHIPYARD); int naniteFactoryLevel = body.getBuildingLevel(BuildingKind.NANITE_FACTORY); return itemTimeUtils.getUnitConstructionTime(cost, shipyardLevel, naniteFactoryLevel); } }