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.ItemCostUtils; 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.building.BuildingItem; import com.github.retro_game.retro_game.repository.BodyRepository; import com.github.retro_game.retro_game.repository.BuildingQueueEntryRepository; import com.github.retro_game.retro_game.repository.EventRepository; 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.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import javax.annotation.PostConstruct; import java.time.Instant; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @Service class BuildingsServiceImpl implements BuildingsServiceInternal { private static final Logger logger = LoggerFactory.getLogger(BuildingsServiceImpl.class); private final int buildingQueueCapacity; private final int fieldsPerTerraformerLevel; private final int fieldsPerLunarBaseLevel; private final ItemTimeUtils itemTimeUtils; private final BodyRepository bodyRepository; private final BuildingQueueEntryRepository buildingQueueEntryRepository; private final EventRepository eventRepository; private BodyServiceInternal bodyServiceInternal; private EventScheduler eventScheduler; private class State { final Map<BuildingKind, Integer> buildings; int usedFields; int maxFields; State(Body body, SortedMap<Integer, BuildingQueueEntry> queue) { buildings = new EnumMap<>(BuildingKind.class); for (var entry : body.getBuildings().entrySet()) { var level = entry.getValue(); buildings.put(entry.getKey(), level); usedFields += level; } maxFields = bodyServiceInternal.getMaxFields(body, buildings); if (queue != null) { for (BuildingQueueEntry entry : queue.values()) { if (entry.getAction() == BuildingQueueAction.CONSTRUCT) { construct(entry.getKind()); } else { assert entry.getAction() == BuildingQueueAction.DESTROY; destroy(entry.getKind()); } } } } void construct(BuildingKind kind) { buildings.put(kind, buildings.getOrDefault(kind, 0) + 1); usedFields++; if (kind == BuildingKind.TERRAFORMER) { maxFields += fieldsPerTerraformerLevel; } else if (kind == BuildingKind.LUNAR_BASE) { maxFields += fieldsPerLunarBaseLevel; } } void destroy(BuildingKind kind) { // A terraformer and lunar base cannot be destroyed once built. assert kind != BuildingKind.TERRAFORMER && kind != BuildingKind.LUNAR_BASE; assert buildings.containsKey(kind) && buildings.get(kind) >= 1; buildings.put(kind, buildings.get(kind) - 1); usedFields--; } } public BuildingsServiceImpl(@Value("${retro-game.building-queue-capacity}") int buildingQueueCapacity, @Value("${retro-game.fields-per-terraformer-level}") int fieldsPerTerraformerLevel, @Value("${retro-game.fields-per-lunar-base-level}") int fieldsPerLunarBaseLevel, ItemTimeUtils itemTimeUtils, BodyRepository bodyRepository, BuildingQueueEntryRepository buildingQueueEntryRepository, EventRepository eventRepository) { this.buildingQueueCapacity = buildingQueueCapacity; this.fieldsPerTerraformerLevel = fieldsPerTerraformerLevel; this.fieldsPerLunarBaseLevel = fieldsPerLunarBaseLevel; this.itemTimeUtils = itemTimeUtils; this.bodyRepository = bodyRepository; this.buildingQueueEntryRepository = buildingQueueEntryRepository; this.eventRepository = eventRepository; } @Autowired public void setBodyServiceInternal(BodyServiceInternal bodyServiceInternal) { this.bodyServiceInternal = bodyServiceInternal; } @Autowired public void setEventScheduler(EventScheduler eventScheduler) { this.eventScheduler = eventScheduler; } @PostConstruct private void checkProperties() { Assert.isTrue(buildingQueueCapacity >= 1, "retro-game.building-queue-capacity must be at least 1"); Assert.isTrue(fieldsPerTerraformerLevel > 1, "retro-game.fields-per-terraformer-level must be greater than 1"); Assert.isTrue(fieldsPerLunarBaseLevel > 1, "retro-game.fields-per-lunar-base-level must be greater than 1"); } @Override @Transactional(isolation = Isolation.REPEATABLE_READ, readOnly = true) public BuildingsAndQueuePairDto getBuildingsAndQueuePair(long bodyId) { Body body = bodyServiceInternal.getUpdated(bodyId); User user = body.getUser(); Resources resources = body.getResources(); final int totalEnergy = bodyServiceInternal.getProduction(body).getTotalEnergy(); State state = new State(body, null); SortedMap<Integer, BuildingQueueEntry> buildingQueue = body.getBuildingQueue(); int size = buildingQueue.size(); List<BuildingQueueEntryDto> queue = new ArrayList<>(size); if (size > 0) { Iterator<Map.Entry<Integer, BuildingQueueEntry>> it = buildingQueue.entrySet().iterator(); Map.Entry<Integer, BuildingQueueEntry> next = it.next(); boolean first = true; long finishAt = 0; boolean upMovable = false; do { Map.Entry<Integer, BuildingQueueEntry> entry = next; next = it.hasNext() ? it.next() : null; BuildingQueueEntry queueEntry = entry.getValue(); BuildingKind kind = queueEntry.getKind(); BuildingQueueAction action = queueEntry.getAction(); int levelFrom = state.buildings.getOrDefault(kind, 0); assert action == BuildingQueueAction.CONSTRUCT || action == BuildingQueueAction.DESTROY; int levelTo = levelFrom + (action == BuildingQueueAction.CONSTRUCT ? 1 : -1); assert levelTo >= 0; var cost = ItemCostUtils.getCost(kind, levelTo); var requiredEnergy = ItemCostUtils.getRequiredEnergy(kind, levelTo); long requiredTime; if (first) { Optional<Event> event = eventRepository.findFirstByKindAndParam(EventKind.BUILDING_QUEUE, bodyId); Assert.isTrue(event.isPresent(), "Event must be present"); finishAt = event.get().getAt().toInstant().getEpochSecond(); long now = body.getUpdatedAt().toInstant().getEpochSecond(); requiredTime = finishAt - now; } else { if (action == BuildingQueueAction.CONSTRUCT) { requiredTime = getConstructionTime(cost, state.buildings); } else { requiredTime = getDestructionTime(cost, state.buildings); } finishAt += requiredTime; } // Check dependencies of subsequent entries. SortedMap<Integer, BuildingQueueEntry> tail = buildingQueue.tailMap(entry.getKey()); boolean downMovable = canSwapTop(state, tail); boolean cancelable = canRemoveTop(state, tail); // Moving down or cancelling the first entry is equivalent to building the second one, which is the reason // for checking resources. if (first && next != null) { BuildingQueueAction nextAction = next.getValue().getAction(); BuildingKind nextKind = next.getValue().getKind(); assert nextAction == BuildingQueueAction.CONSTRUCT || nextAction == BuildingQueueAction.DESTROY; int nextLevel = state.buildings.getOrDefault(nextKind, 0) + (nextAction == BuildingQueueAction.CONSTRUCT ? 1 : -1); assert nextLevel >= 0; var nextCost = ItemCostUtils.getCost(nextKind, nextLevel); nextCost.sub(cost); if (!resources.greaterOrEqual(nextCost)) { downMovable = cancelable = false; } var nextRequiredEnergy = ItemCostUtils.getRequiredEnergy(nextKind, nextLevel); if (nextRequiredEnergy > totalEnergy) { downMovable = cancelable = false; } var nextItem = Item.get(nextKind); if (!ItemRequirementsUtils.meetsTechnologiesRequirements(nextItem, user)) { downMovable = cancelable = false; } } queue.add(new BuildingQueueEntryDto(Converter.convert(kind), entry.getKey(), levelFrom, levelTo, Converter.convert(cost), requiredEnergy, Date.from(Instant.ofEpochSecond(finishAt)), requiredTime, downMovable, upMovable, cancelable)); if (action == BuildingQueueAction.CONSTRUCT) { state.construct(kind); } else { state.destroy(kind); } first = false; upMovable = downMovable; } while (next != null); } boolean canConstruct = state.usedFields < state.maxFields && queue.size() < buildingQueueCapacity; List<BuildingDto> buildings = new ArrayList<>(); for (Map.Entry<BuildingKind, BuildingItem> entry : BuildingItem.getAll().entrySet()) { BuildingKind kind = entry.getKey(); BuildingItem item = entry.getValue(); boolean meetsRequirements = item.meetsSpecialRequirements(body) && ItemRequirementsUtils.meetsBuildingsRequirements(item, state.buildings) && (!queue.isEmpty() || ItemRequirementsUtils.meetsTechnologiesRequirements(item, user)); var futureLevel = state.buildings.get(kind); if (meetsRequirements || futureLevel > 0) { var currentLevel = body.getBuildingLevel(kind); var cost = ItemCostUtils.getCost(kind, futureLevel + 1); var requiredEnergy = ItemCostUtils.getRequiredEnergy(kind, futureLevel + 1); long constructionTime = getConstructionTime(cost, state.buildings); boolean canConstructNow = canConstruct && meetsRequirements && (!queue.isEmpty() || (resources.greaterOrEqual(cost) && totalEnergy >= requiredEnergy)); buildings.add(new BuildingDto(Converter.convert(kind), currentLevel, futureLevel, Converter.convert(cost), requiredEnergy, constructionTime, canConstructNow)); } } // Keep the order defined in the service layer. buildings.sort(Comparator.comparing(BuildingDto::getKind)); return new BuildingsAndQueuePairDto(buildings, queue); } @Override public Map<BuildingKind, Tuple2<Integer, Integer>> getCurrentAndFutureLevels(Body body) { State state = new State(body, body.getBuildingQueue()); return Arrays.stream(BuildingKind.values()) .filter(kind -> body.getBuildingLevel(kind) != 0 || state.buildings.getOrDefault(kind, 0) != 0) .collect(Collectors.toMap( Function.identity(), kind -> Tuple.of(body.getBuildingLevel(kind), state.buildings.getOrDefault(kind, 0)), (a, b) -> { throw new IllegalStateException(); }, () -> new EnumMap<>(BuildingKind.class) )); } @Override public Optional<OngoingBuildingDto> getOngoingBuilding(Body body) { SortedMap<Integer, BuildingQueueEntry> buildingQueue = body.getBuildingQueue(); if (buildingQueue.isEmpty()) { return Optional.empty(); } BuildingQueueEntry first = buildingQueue.get(buildingQueue.firstKey()); assert first != null; BuildingKind kind = first.getKind(); BuildingQueueAction action = first.getAction(); var level = body.getBuildingLevel(kind) + (action == BuildingQueueAction.CONSTRUCT ? 1 : -1); return Optional.of(new OngoingBuildingDto(kind, level)); } @Override public Optional<Date> getOngoingBuildingFinishAt(Body body) { return eventRepository.findFirstByKindAndParam(EventKind.BUILDING_QUEUE, body.getId()).map(Event::getAt); } @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public void construct(long bodyId, BuildingKindDto kind) { BuildingKind k = Converter.convert(kind); Body body = bodyServiceInternal.getUpdated(bodyId); SortedMap<Integer, BuildingQueueEntry> queue = body.getBuildingQueue(); if (queue.size() >= buildingQueueCapacity) { logger.warn("Constructing building failed, queue is full: bodyId={} kind={}", bodyId, k); throw new QueueFullException(); } State state = new State(body, queue); if (state.usedFields >= state.maxFields) { logger.warn("Constructing building failed, no more free fields: bodyId={} kind={}", bodyId, k); throw new NoMoreFreeFieldsException(); } var item = Item.get(k); if (!item.meetsSpecialRequirements(body) || !ItemRequirementsUtils.meetsBuildingsRequirements(item, state.buildings) || (queue.isEmpty() && !ItemRequirementsUtils.meetsTechnologiesRequirements(item, body.getUser()))) { logger.warn("Constructing building failed, requirements not met: bodyId={} kind={}", bodyId, k); throw new RequirementsNotMetException(); } BuildingQueueEntryKey key = new BuildingQueueEntryKey(); key.setBody(body); if (!queue.isEmpty()) { int sequenceNumber = queue.lastKey() + 1; key.setSequence(sequenceNumber); logger.info("Constructing building successful, appending to queue: bodyId={} kind={} sequenceNumber={}", bodyId, k, sequenceNumber); } else { int level = state.buildings.getOrDefault(k, 0) + 1; var cost = ItemCostUtils.getCost(k, level); if (!body.getResources().greaterOrEqual(cost)) { logger.warn("Constructing building failed, not enough resources: bodyId={} kind={}", bodyId, k); throw new NotEnoughResourcesException(); } body.getResources().sub(cost); var requiredEnergy = ItemCostUtils.getRequiredEnergy(k, level); if (requiredEnergy > 0) { int totalEnergy = bodyServiceInternal.getProduction(body).getTotalEnergy(); if (requiredEnergy > totalEnergy) { logger.warn("Constructing building failed, not enough energy: bodyId={} kind={}", bodyId, k); throw new NotEnoughEnergyException(); } } logger.info("Constructing building successful, creating a new event: bodyId={} kind={}", bodyId, k); Date now = body.getUpdatedAt(); long requiredTime = getConstructionTime(cost, state.buildings); Date startAt = Date.from(Instant.ofEpochSecond(now.toInstant().getEpochSecond() + requiredTime)); Event event = new Event(); event.setAt(startAt); event.setKind(EventKind.BUILDING_QUEUE); event.setParam(bodyId); eventScheduler.schedule(event); key.setSequence(1); } BuildingQueueEntry entry = new BuildingQueueEntry(); entry.setKey(key); entry.setKind(k); entry.setAction(BuildingQueueAction.CONSTRUCT); buildingQueueEntryRepository.save(entry); } @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public void destroy(long bodyId, BuildingKindDto kind) { BuildingKind k = Converter.convert(kind); Body body = bodyServiceInternal.getUpdated(bodyId); if (k == BuildingKind.TERRAFORMER || k == BuildingKind.LUNAR_BASE) { logger.warn("Destroying building failed, cannot destroy this building: bodyId={} kind={}", bodyId, k); throw new WrongBuildingKindException(); } SortedMap<Integer, BuildingQueueEntry> queue = body.getBuildingQueue(); if (queue.size() >= buildingQueueCapacity) { logger.warn("Destroying building failed, queue is full: bodyId={} kind={}", bodyId, k); throw new QueueFullException(); } State state = new State(body, queue); if (state.buildings.getOrDefault(k, 0) == 0) { logger.warn("Destroying building failed, the building is already going to be fully destroyed: bodyId={} kind={}", bodyId, k); throw new BuildingAlreadyDestroyedException(); } BuildingQueueEntryKey key = new BuildingQueueEntryKey(); key.setBody(body); if (!queue.isEmpty()) { int sequenceNumber = queue.lastKey() + 1; key.setSequence(sequenceNumber); logger.info("Destroying building successful, appending to queue: bodyId={} kind={} sequenceNumber={}", bodyId, k, sequenceNumber); } else { assert state.buildings.containsKey(k); int level = state.buildings.get(k) - 1; assert level >= 0; var cost = ItemCostUtils.getCost(k, level); if (!body.getResources().greaterOrEqual(cost)) { logger.warn("Destroying building failed, not enough resources: bodyId={} kind={}", bodyId, k); throw new NotEnoughResourcesException(); } body.getResources().sub(cost); var requiredEnergy = ItemCostUtils.getRequiredEnergy(k, level); if (requiredEnergy > 0) { int totalEnergy = bodyServiceInternal.getProduction(body).getTotalEnergy(); if (requiredEnergy > totalEnergy) { logger.warn("Destroying building failed, not enough energy: bodyId={} kind={}", bodyId, k); throw new NotEnoughEnergyException(); } } logger.info("Destroying building successful, create a new event: bodyId={} kind={}", bodyId, k); Date now = body.getUpdatedAt(); long requiredTime = getDestructionTime(cost, state.buildings); Date startAt = Date.from(Instant.ofEpochSecond(now.toInstant().getEpochSecond() + requiredTime)); Event event = new Event(); event.setAt(startAt); event.setKind(EventKind.BUILDING_QUEUE); event.setParam(bodyId); eventScheduler.schedule(event); key.setSequence(1); } BuildingQueueEntry entry = new BuildingQueueEntry(); entry.setKey(key); entry.setKind(k); entry.setAction(BuildingQueueAction.DESTROY); buildingQueueEntryRepository.save(entry); } @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public void moveDown(long bodyId, int sequenceNumber) { Body body = bodyServiceInternal.getUpdated(bodyId); SortedMap<Integer, BuildingQueueEntry> queue = body.getBuildingQueue(); if (!queue.containsKey(sequenceNumber)) { logger.warn("Moving down entry in building queue failed, no such queue entry: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); throw new NoSuchQueueEntryException(); } SortedMap<Integer, BuildingQueueEntry> head = queue.headMap(sequenceNumber); SortedMap<Integer, BuildingQueueEntry> tail = queue.tailMap(sequenceNumber); State state = new State(body, head); if (!canSwapTop(state, tail)) { logger.warn("Moving down entry in building queue failed, cannot swap top: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); throw new CannotMoveException(); } // canSwapTop == true implies tail.size >= 2. assert tail.size() >= 2; Iterator<BuildingQueueEntry> it = tail.values().iterator(); BuildingQueueEntry entry = it.next(); BuildingQueueEntry next = it.next(); if (!head.isEmpty()) { // The entry is not the first in the queue, just swap it with the next. logger.info("Moving down entry in building queue successful, the entry isn't the first: bodyId={}" + " sequenceNumber={}", bodyId, sequenceNumber); } else { // The first entry. BuildingKind firstKind = entry.getKind(); BuildingQueueAction firstAction = entry.getAction(); assert firstAction == BuildingQueueAction.CONSTRUCT || firstAction == BuildingQueueAction.DESTROY; int firstLevel = state.buildings.getOrDefault(firstKind, 0) + (firstAction == BuildingQueueAction.CONSTRUCT ? 1 : -1); assert firstLevel >= 0; var firstCost = ItemCostUtils.getCost(firstKind, firstLevel); BuildingKind secondKind = next.getKind(); BuildingQueueAction secondAction = next.getAction(); assert secondAction == BuildingQueueAction.CONSTRUCT || secondAction == BuildingQueueAction.DESTROY; int secondLevel = state.buildings.getOrDefault(secondKind, 0) + (secondAction == BuildingQueueAction.CONSTRUCT ? 1 : -1); assert secondLevel >= 0; var secondCost = ItemCostUtils.getCost(secondKind, secondLevel); body.getResources().add(firstCost); if (!body.getResources().greaterOrEqual(secondCost)) { logger.warn("Moving down entry in building queue failed, not enough resources: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); throw new NotEnoughResourcesException(); } body.getResources().sub(secondCost); var requiredEnergy = ItemCostUtils.getRequiredEnergy(secondKind, secondLevel); if (requiredEnergy > 0) { int totalEnergy = bodyServiceInternal.getProduction(body).getTotalEnergy(); if (requiredEnergy > totalEnergy) { logger.warn("Moving down entry in building queue failed, not enough energy: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); throw new NotEnoughEnergyException(); } } var secondItem = Item.get(secondKind); if (!ItemRequirementsUtils.meetsTechnologiesRequirements(secondItem, body.getUser())) { logger.warn("Moving down entry in building queue failed, requirements not met: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); throw new RequirementsNotMetException(); } Optional<Event> eventOptional = eventRepository.findFirstByKindAndParam(EventKind.BUILDING_QUEUE, bodyId); if (!eventOptional.isPresent()) { logger.error("Moving down entry in building queue failed, the event is not present: bodyId={}" + " sequenceNumber={}", bodyId, sequenceNumber); throw new MissingEventException(); } Event event = eventOptional.get(); long requiredTime = secondAction == BuildingQueueAction.CONSTRUCT ? getConstructionTime(secondCost, state.buildings) : getDestructionTime(secondCost, state.buildings); logger.info("Moving down entry in building queue successful, the entry is the first, adding an event for the" + " next entry: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); Date now = body.getUpdatedAt(); Date at = Date.from(Instant.ofEpochSecond(now.toInstant().getEpochSecond() + requiredTime)); event.setAt(at); eventScheduler.schedule(event); } // Swap. BuildingKind kind = entry.getKind(); BuildingQueueAction action = entry.getAction(); entry.setKind(next.getKind()); entry.setAction(next.getAction()); next.setKind(kind); next.setAction(action); } @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public void moveUp(long bodyId, int sequenceNumber) { Body body = bodyRepository.getOne(bodyId); SortedMap<Integer, BuildingQueueEntry> queue = body.getBuildingQueue(); if (!queue.containsKey(sequenceNumber)) { logger.warn("Moving up entry in building queue failed, no such queue entry: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); throw new NoSuchQueueEntryException(); } SortedMap<Integer, BuildingQueueEntry> head = queue.headMap(sequenceNumber); if (head.isEmpty()) { logger.warn("Moving up entry in building queue failed, the entry is first: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); throw new CannotMoveException(); } // Moving an entry up is equivalent to moving the previous one down. moveDown(bodyId, head.lastKey()); } @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public void cancel(long bodyId, int sequenceNumber) { Body body = bodyServiceInternal.getUpdated(bodyId); SortedMap<Integer, BuildingQueueEntry> queue = body.getBuildingQueue(); if (!queue.containsKey(sequenceNumber)) { logger.warn("Cancelling entry in building queue failed, no such queue entry: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); throw new NoSuchQueueEntryException(); } SortedMap<Integer, BuildingQueueEntry> head = queue.headMap(sequenceNumber); SortedMap<Integer, BuildingQueueEntry> tail = queue.tailMap(sequenceNumber); State state = new State(body, head); if (!canRemoveTop(state, tail)) { logger.warn("Cancelling entry in building queue failed, cannot remove top: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); throw new CannotCancelException(); } if (!head.isEmpty()) { // The entry is not the first in the queue, just remove it. logger.info("Cancelling entry in building queue successful, the entry isn't the first: bodyId={}" + " sequenceNumber={}", bodyId, sequenceNumber); BuildingQueueEntry entry = queue.remove(sequenceNumber); buildingQueueEntryRepository.delete(entry); } else { // The first entry. Iterator<BuildingQueueEntry> it = tail.values().iterator(); BuildingQueueEntry entry = it.next(); BuildingKind kind = entry.getKind(); BuildingQueueAction action = entry.getAction(); assert action == BuildingQueueAction.CONSTRUCT || action == BuildingQueueAction.DESTROY; int level = state.buildings.getOrDefault(kind, 0) + (action == BuildingQueueAction.CONSTRUCT ? 1 : -1); assert level >= 0; var cost = ItemCostUtils.getCost(kind, level); body.getResources().add(cost); Optional<Event> eventOptional = eventRepository.findFirstByKindAndParam(EventKind.BUILDING_QUEUE, bodyId); if (!eventOptional.isPresent()) { logger.error("Cancelling entry in building queue failed, the event is not present: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); throw new MissingEventException(); } Event event = eventOptional.get(); if (!it.hasNext()) { logger.info("Cancelling entry in building queue successful, queue is empty now: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); eventRepository.delete(event); } else { // Get the next item. BuildingQueueEntry next = it.next(); kind = next.getKind(); action = next.getAction(); assert action == BuildingQueueAction.CONSTRUCT || action == BuildingQueueAction.DESTROY; level = state.buildings.getOrDefault(kind, 0) + (action == BuildingQueueAction.CONSTRUCT ? 1 : -1); assert level >= 0; cost = ItemCostUtils.getCost(kind, level); if (!body.getResources().greaterOrEqual(cost)) { logger.warn("Cancelling entry in building queue failed, not enough resources: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); throw new NotEnoughResourcesException(); } body.getResources().sub(cost); var requiredEnergy = ItemCostUtils.getRequiredEnergy(kind, level); if (requiredEnergy > 0) { int totalEnergy = bodyServiceInternal.getProduction(body).getTotalEnergy(); if (requiredEnergy > totalEnergy) { logger.warn("Cancelling entry in building queue failed, not enough energy: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); throw new NotEnoughEnergyException(); } } var item = Item.get(kind); if (!ItemRequirementsUtils.meetsTechnologiesRequirements(item, body.getUser())) { logger.warn("Cancelling entry in building queue failed, requirements not met: bodyId={} sequenceNumber={}", bodyId, sequenceNumber); throw new RequirementsNotMetException(); } long requiredTime = action == BuildingQueueAction.CONSTRUCT ? getConstructionTime(cost, state.buildings) : getDestructionTime(cost, state.buildings); logger.info("Cancelling entry in building queue successful, the entry is the first, modifying the event:" + " bodyId={} sequenceNumber={}", bodyId, sequenceNumber); Date now = body.getUpdatedAt(); Date at = Date.from(Instant.ofEpochSecond(now.toInstant().getEpochSecond() + requiredTime)); event.setAt(at); eventScheduler.schedule(event); } it.remove(); buildingQueueEntryRepository.delete(entry); } } @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); Collection<BuildingQueueEntry> values = body.getBuildingQueue().values(); Iterator<BuildingQueueEntry> it = values.iterator(); // This shouldn't happen. if (!it.hasNext()) { logger.error("Handling building queue, queue is empty: bodyId={}", bodyId); return; } BuildingQueueEntry entry = it.next(); BuildingKind kind = entry.getKind(); it.remove(); buildingQueueEntryRepository.delete(entry); bodyServiceInternal.updateResources(body, at); // Update buildings. var oldLevel = body.getBuildingLevel(kind); assert oldLevel >= 0; var newLevel = oldLevel + (entry.getAction() == BuildingQueueAction.CONSTRUCT ? 1 : -1); assert newLevel >= 0; logger.info("Handling building queue, updating building level: bodyId={} kind={} oldLevel={} newLevel={}", bodyId, kind, oldLevel, newLevel); body.setBuildingLevel(kind, newLevel); // Handle subsequent entries. final int totalEnergy = bodyServiceInternal.getProduction(body).getTotalEnergy(); final int usedFields = bodyServiceInternal.getUsedFields(body); final int maxFields = bodyServiceInternal.getMaxFields(body); while (it.hasNext()) { entry = it.next(); kind = entry.getKind(); BuildingQueueAction action = entry.getAction(); int sequenceNumber = entry.getSequence(); if (action == BuildingQueueAction.CONSTRUCT && usedFields >= maxFields) { logger.info("Handling building queue, removing entry, no more free fields: bodyId={} kind={} sequenceNumber={}", bodyId, kind, sequenceNumber); it.remove(); buildingQueueEntryRepository.delete(entry); continue; } var level = body.getBuildingLevel(kind); assert level >= 0; if (action == BuildingQueueAction.DESTROY && level == 0) { logger.error("Handling building queue, destroying non-existing building: bodyId={} kind={} sequenceNumber={}", bodyId, kind, sequenceNumber); it.remove(); buildingQueueEntryRepository.delete(entry); continue; } assert action == BuildingQueueAction.CONSTRUCT || action == BuildingQueueAction.DESTROY; level += action == BuildingQueueAction.CONSTRUCT ? 1 : -1; var cost = ItemCostUtils.getCost(kind, level); if (!body.getResources().greaterOrEqual(cost)) { logger.info("Handling building queue, removing entry, not enough resources: bodyId={} kind={}" + " sequenceNumber={}", bodyId, kind, sequenceNumber); it.remove(); buildingQueueEntryRepository.delete(entry); continue; } var requiredEnergy = ItemCostUtils.getRequiredEnergy(kind, level); if (requiredEnergy > totalEnergy) { logger.info("Handling building queue, removing entry, not enough energy: bodyId={} kind={}" + " sequenceNumber={}", bodyId, kind, sequenceNumber); it.remove(); buildingQueueEntryRepository.delete(entry); continue; } var item = Item.get(kind); if (!ItemRequirementsUtils.meetsRequirements(item, body)) { logger.info("Handling building queue, removing entry, requirements not met: bodyId={} kind={}" + " sequenceNumber={}", bodyId, kind, sequenceNumber); it.remove(); buildingQueueEntryRepository.delete(entry); continue; } logger.info("Handling building queue, creating an event: bodyId={} kind={} action={} level={} sequenceNumber={}", bodyId, kind, action, level, sequenceNumber); body.getResources().sub(cost); long requiredTime = action == BuildingQueueAction.CONSTRUCT ? getConstructionTime(cost, body) : getDestructionTime(cost, body); Date startAt = Date.from(Instant.ofEpochSecond(at.toInstant().getEpochSecond() + requiredTime)); Event newEvent = new Event(); newEvent.setAt(startAt); newEvent.setKind(EventKind.BUILDING_QUEUE); newEvent.setParam(bodyId); eventScheduler.schedule(newEvent); break; } } @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public void deleteBuildingsAndQueue(Body body) { Optional<Event> event = eventRepository.findFirstByKindAndParam(EventKind.BUILDING_QUEUE, body.getId()); event.ifPresent(eventRepository::delete); buildingQueueEntryRepository.deleteAll(body.getBuildingQueue().values()); } private long getConstructionTime(Resources cost, Body body) { var roboticsFactoryLevel = body.getBuildingLevel(BuildingKind.ROBOTICS_FACTORY); var naniteFactoryLevel = body.getBuildingLevel(BuildingKind.NANITE_FACTORY); return itemTimeUtils.getBuildingConstructionTime(cost, roboticsFactoryLevel, naniteFactoryLevel); } private long getConstructionTime(Resources cost, Map<BuildingKind, Integer> buildings) { var roboticsFactoryLevel = buildings.getOrDefault(BuildingKind.ROBOTICS_FACTORY, 0); var naniteFactoryLevel = buildings.getOrDefault(BuildingKind.NANITE_FACTORY, 0); return itemTimeUtils.getBuildingConstructionTime(cost, roboticsFactoryLevel, naniteFactoryLevel); } private long getDestructionTime(Resources cost, Body body) { var roboticsFactoryLevel = body.getBuildingLevel(BuildingKind.ROBOTICS_FACTORY); var naniteFactoryLevel = body.getBuildingLevel(BuildingKind.NANITE_FACTORY); return itemTimeUtils.getBuildingDestructionTime(cost, roboticsFactoryLevel, naniteFactoryLevel); } private long getDestructionTime(Resources cost, Map<BuildingKind, Integer> buildings) { var roboticsFactoryLevel = buildings.getOrDefault(BuildingKind.ROBOTICS_FACTORY, 0); var naniteFactoryLevel = buildings.getOrDefault(BuildingKind.NANITE_FACTORY, 0); return itemTimeUtils.getBuildingDestructionTime(cost, roboticsFactoryLevel, naniteFactoryLevel); } // Checks whether it is possible to swap top two items in the queue ignoring resources. private boolean canSwapTop(State state, SortedMap<Integer, BuildingQueueEntry> queue) { if (queue.size() < 2) { return false; } // Get first two items. Iterator<BuildingQueueEntry> it = queue.values().iterator(); BuildingQueueEntry first = it.next(); BuildingQueueEntry second = it.next(); // Check requirements. // The second building will always meet requirements when the first action is destroy. if (first.getAction() == BuildingQueueAction.CONSTRUCT) { if (second.getAction() == BuildingQueueAction.CONSTRUCT) { var requirements = Item.get(second.getKind()).getBuildingsRequirements(); if (requirements.getOrDefault(first.getKind(), 0) > state.buildings.getOrDefault(first.getKind(), 0)) { return false; } } else { assert second.getAction() == BuildingQueueAction.DESTROY; if (!state.buildings.containsKey(second.getKind())) { return false; } var requirements = Item.get(first.getKind()).getBuildingsRequirements(); int levelAfterDeconstruction = state.buildings.get(second.getKind()) - 1; assert levelAfterDeconstruction >= 0; if (requirements.getOrDefault(second.getKind(), 0) > levelAfterDeconstruction) { return false; } } } // Check body's fields. // If the second action is destroy, there will be always enough free fields after the swap. if (second.getAction() == BuildingQueueAction.CONSTRUCT) { if (first.getAction() == BuildingQueueAction.DESTROY) { // The destruction can free one field and thus we can construct the second one, but after the swap there may not // be enough fields to construct it. if (state.usedFields >= state.maxFields) { return false; } } else if ((first.getKind() == BuildingKind.TERRAFORMER || first.getKind() == BuildingKind.LUNAR_BASE) && second.getKind() != BuildingKind.TERRAFORMER && second.getKind() != BuildingKind.LUNAR_BASE && state.usedFields + 1 >= state.maxFields) { // After the second one would be built, there won't be free fields anymore, as the second one doesn't increase // the max fields like the first one. return false; } } return true; } private boolean canRemoveTop(State state, SortedMap<Integer, BuildingQueueEntry> queue) { if (queue.isEmpty()) { return false; } Iterator<BuildingQueueEntry> it = queue.values().iterator(); BuildingKind firstKind = it.next().getKind(); int usedFields = state.usedFields; int maxFields = state.maxFields; int level = state.buildings.getOrDefault(firstKind, 0); while (it.hasNext()) { // Check whether there is enough fields to construct the building. This must be checked, because we may remove // construction of a terraformer or lunar base, or remove destruction of a building. if (usedFields >= maxFields) { return false; } usedFields++; BuildingQueueEntry current = it.next(); BuildingQueueAction currentAction = current.getAction(); BuildingKind currentKind = current.getKind(); // Increase the max fields. // Terraformer and lunar base cannot be destroyed once built. assert !(currentKind == BuildingKind.TERRAFORMER || currentKind == BuildingKind.LUNAR_BASE) || currentAction == BuildingQueueAction.CONSTRUCT; if (currentKind == BuildingKind.TERRAFORMER) { maxFields += fieldsPerTerraformerLevel; } else if (currentKind == BuildingKind.LUNAR_BASE) { maxFields += fieldsPerLunarBaseLevel; } if (currentKind == firstKind) { if (currentAction == BuildingQueueAction.CONSTRUCT) { level++; } else { assert currentAction == BuildingQueueAction.DESTROY; if (level == 0) { return false; } level--; } } else { var requirements = Item.get(currentKind).getBuildingsRequirements(); if (requirements.getOrDefault(firstKind, 0) > level) { return false; } } } return true; } }