Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
import java.util.List;

/**
* Mapper for searching regions by contract type, tags, and price range.
* Mapper for searching regions by contract type, tags, price range, and lease occupancy.
*
* @param tagIds when non-null, only regions with at least one of these tags are included
* @param excludedTagIds when non-null, regions with any of these tags are excluded
* @param excludeRented when true, occupied leaseholds are omitted from results
* @see SearchResultEntity
*/
public interface SearchMapper {
Expand All @@ -20,6 +21,7 @@ public interface SearchMapper {
boolean includeLeasehold,
@Nullable Collection<String> tagIds,
@Nullable Collection<String> excludedTagIds,
boolean excludeRented,
double minPrice,
double maxPrice,
int limit,
Expand All @@ -29,6 +31,7 @@ int searchCount(boolean includeFreehold,
boolean includeLeasehold,
@Nullable Collection<String> tagIds,
@Nullable Collection<String> excludedTagIds,
boolean excludeRented,
double minPrice,
double maxPrice);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ AND NOT EXISTS (
INNER JOIN LeaseholdContract lc ON lc.leaseholdContractId = c.contractId
WHERE lc.price &gt;= #{minPrice}
AND lc.price &lt;= #{maxPrice}
<if test="excludeRented">
AND lc.tenantId IS NULL
</if>
<if test="tagIds != null and tagIds.size() > 0">
AND EXISTS (
SELECT 1 FROM RegionTag rt
Expand Down Expand Up @@ -94,6 +97,7 @@ AND NOT EXISTS (
@Param("includeLeasehold") boolean includeLeasehold,
@Param("tagIds") @Nullable Collection<String> tagIds,
@Param("excludedTagIds") @Nullable Collection<String> excludedTagIds,
@Param("excludeRented") boolean excludeRented,
@Param("minPrice") double minPrice,
@Param("maxPrice") double maxPrice,
@Param("limit") int limit,
Expand Down Expand Up @@ -142,6 +146,9 @@ AND NOT EXISTS (
INNER JOIN LeaseholdContract lc ON lc.leaseholdContractId = c.contractId
WHERE lc.price &gt;= #{minPrice}
AND lc.price &lt;= #{maxPrice}
<if test="excludeRented">
AND lc.tenantId IS NULL
</if>
<if test="tagIds != null and tagIds.size() > 0">
AND EXISTS (
SELECT 1 FROM RegionTag rt
Expand Down Expand Up @@ -170,6 +177,7 @@ int searchCount(@Param("includeFreehold") boolean includeFreehold,
@Param("includeLeasehold") boolean includeLeasehold,
@Param("tagIds") @Nullable Collection<String> tagIds,
@Param("excludedTagIds") @Nullable Collection<String> excludedTagIds,
@Param("excludeRented") boolean excludeRented,
@Param("minPrice") double minPrice,
@Param("maxPrice") double maxPrice);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.github.md5sha256.realty.database.entity.LeaseholdContractEntity;
import io.github.md5sha256.realty.database.entity.OutboundOfferView;
import io.github.md5sha256.realty.database.entity.RealtyRegionEntity;
import io.github.md5sha256.realty.database.entity.SearchResultEntity;
import io.github.md5sha256.realty.database.entity.FreeholdContractAuctionEntity;
import io.github.md5sha256.realty.database.entity.FreeholdContractBid;
import io.github.md5sha256.realty.database.entity.FreeholdContractBidPaymentEntity;
Expand Down Expand Up @@ -228,6 +229,69 @@ void countByTenant() {
}
}

// ==================== SearchMapper ====================

@Nested
@DisplayName("SearchMapper")
class SearchMapperTests {

@Test
@DisplayName("excludeRented omits occupied leaseholds from search results")
void excludeRentedFiltersOccupiedLeaseholds() {
String availableLeasehold = uniqueRegionId();
createLeaseholdRegion(availableLeasehold, AUTHORITY);

String rentedLeasehold = uniqueRegionId();
createLeaseholdRegion(rentedLeasehold, AUTHORITY);
logic.rentRegion(rentedLeasehold, WORLD_ID, PLAYER_A);

try (SqlSessionWrapper wrapper = database.openSession()) {
int countIncludingRented = wrapper.searchMapper()
.searchCount(false, true, null, null,
false, 0.0, Double.MAX_VALUE);
int countExcludingRented = wrapper.searchMapper()
.searchCount(false, true, null, null,
true, 0.0, Double.MAX_VALUE);
List<SearchResultEntity> availableOnly = wrapper.searchMapper()
.search(false, true, null, null,
true, 0.0, Double.MAX_VALUE, 10, 0);

Assertions.assertEquals(2, countIncludingRented);
Assertions.assertEquals(1, countExcludingRented);
Assertions.assertEquals(1, availableOnly.size());
Assertions.assertEquals(availableLeasehold, availableOnly.get(0).worldGuardRegionId());
}
}

@Test
@DisplayName("excludeRented does not hide freeholds")
void excludeRentedKeepsFreeholds() {
String freehold = uniqueRegionId();
boolean created = logic.createFreehold(freehold, WORLD_ID, 1000.0, AUTHORITY, null);
Assertions.assertTrue(created);

String availableLeasehold = uniqueRegionId();
createLeaseholdRegion(availableLeasehold, AUTHORITY);

String rentedLeasehold = uniqueRegionId();
createLeaseholdRegion(rentedLeasehold, AUTHORITY);
logic.rentRegion(rentedLeasehold, WORLD_ID, PLAYER_A);

try (SqlSessionWrapper wrapper = database.openSession()) {
List<SearchResultEntity> results = wrapper.searchMapper()
.search(true, true, null, null,
true, 0.0, Double.MAX_VALUE, 10, 0);
List<String> regionIds = results.stream()
.map(SearchResultEntity::worldGuardRegionId)
.toList();

Assertions.assertTrue(regionIds.contains(freehold));
Assertions.assertTrue(regionIds.contains(availableLeasehold));
Assertions.assertFalse(regionIds.contains(rentedLeasehold));
}
}
}

// ==================== FreeholdContractMapper ====================

@Nested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ public record SearchCommand(
.withComponent(StringParser.stringParser())
.build();

private static final CommandFlag<Void> EXCLUDE_RENTED_FLAG =
CommandFlag.<Source>builder("exclude-rented")
.build();

private static final CommandFlag<Double> MIN_PRICE_FLAG =
CommandFlag.<Source>builder("min-price")
.withComponent(DoubleParser.doubleParser(0))
Expand Down Expand Up @@ -77,6 +81,7 @@ public record SearchCommand(
.flag(LEASEHOLD_FLAG)
.flag(TAGS_FLAG)
.flag(EXCLUDE_TAGS_FLAG)
.flag(EXCLUDE_RENTED_FLAG)
.flag(MIN_PRICE_FLAG)
.flag(MAX_PRICE_FLAG)
.flag(PAGE_FLAG)
Expand Down Expand Up @@ -104,12 +109,13 @@ private void executeResults(@NotNull CommandContext<? extends Source> ctx) {
}
Collection<String> tagIds = parseTagIds(ctx.flags().getValue(TAGS_FLAG, null));
Collection<String> excludedTagIds = parseTagIds(ctx.flags().getValue(EXCLUDE_TAGS_FLAG, null));
boolean excludeRented = ctx.flags().hasFlag(EXCLUDE_RENTED_FLAG);
double minPrice = ctx.flags().getValue(MIN_PRICE_FLAG, 0.0);
double maxPrice = ctx.flags().getValue(MAX_PRICE_FLAG, Double.MAX_VALUE);
int page = ctx.flags().getValue(PAGE_FLAG, 1);

searchDialog.performSearch(sender, includeFreehold, includeLeasehold, tagIds,
excludedTagIds, minPrice, maxPrice, page);
excludedTagIds, excludeRented, minPrice, maxPrice, page);
}

@Nullable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public final class SearchDialog {

static final String INPUT_FREEHOLD = "freehold";
static final String INPUT_LEASEHOLD = "leasehold";
static final String INPUT_EXCLUDE_RENTED = "exclude_rented";
static final String INPUT_MIN_PRICE = "min_price";
static final String INPUT_MAX_PRICE = "max_price";

Expand All @@ -85,6 +86,7 @@ TagState next() {
static final class SearchState {
boolean freehold = true;
boolean leasehold = true;
boolean excludeRented = false;
String minPrice = "0";
String maxPrice = "";
final Map<String, TagState> tagStates = new LinkedHashMap<>();
Expand Down Expand Up @@ -129,6 +131,11 @@ private void showMainDialog(@NotNull Player player,
.onTrue("true")
.onFalse("false")
.build());
inputs.add(DialogInput.bool(INPUT_EXCLUDE_RENTED, Component.text("Exclude Rented Leaseholds"))
.initial(state.excludeRented)
.onTrue("true")
.onFalse("false")
.build());
inputs.add(DialogInput.text(INPUT_MIN_PRICE, Component.text("Min Price"))
.width(150)
.initial(state.minPrice)
Expand Down Expand Up @@ -161,14 +168,15 @@ private void showMainDialog(@NotNull Player player,
double maxPrice = parsePrice(state.maxPrice, Double.MAX_VALUE);
boolean includeFreehold = state.freehold;
boolean includeLeasehold = state.leasehold;
boolean excludeRented = state.excludeRented;
playerStates.remove(player.getUniqueId());

if (!includeFreehold && !includeLeasehold) {
audience.sendMessage(messages.messageFor(MessageKeys.SEARCH_NO_RESULTS));
return;
}
performSearch(audience, includeFreehold, includeLeasehold, tagFilter,
excludeFilter, minPrice, maxPrice, 1);
excludeFilter, excludeRented, minPrice, maxPrice, 1);
};

DialogActionCallback configTagsCallback = (response, audience) -> {
Expand All @@ -193,7 +201,7 @@ private void showMainDialog(@NotNull Player player,
.canCloseWithEscape(true)
.afterAction(DialogBase.DialogAfterAction.CLOSE)
.body(List.of(DialogBody.plainMessage(
Component.text("Filter regions by type and price range."))))
Component.text("Filter regions by type, occupancy, and price range."))))
.inputs(inputs)
.build())
.type(DialogType.multiAction(
Expand Down Expand Up @@ -280,6 +288,10 @@ private void saveCriteria(@NotNull SearchState state,
state.freehold = freehold == null || freehold;
Boolean leasehold = response.getBoolean(INPUT_LEASEHOLD);
state.leasehold = leasehold == null || leasehold;
Boolean excludeRented = response.getBoolean(INPUT_EXCLUDE_RENTED);
if (excludeRented != null) {
state.excludeRented = excludeRented;
}
String minPrice = response.getText(INPUT_MIN_PRICE);
if (minPrice != null) {
state.minPrice = minPrice;
Expand All @@ -297,12 +309,13 @@ void performSearch(@NotNull Audience sender,
boolean includeFreehold, boolean includeLeasehold,
@Nullable Collection<String> tagIds,
@Nullable Collection<String> excludedTagIds,
boolean excludeRented,
double minPrice, double maxPrice, int page) {
CompletableFuture.runAsync(() -> {
try (SqlSessionWrapper session = database.openSession(true)) {
SearchMapper mapper = session.searchMapper();
int totalCount = mapper.searchCount(includeFreehold, includeLeasehold,
tagIds, excludedTagIds, minPrice, maxPrice);
tagIds, excludedTagIds, excludeRented, minPrice, maxPrice);

if (totalCount == 0) {
sender.sendMessage(messages.messageFor(MessageKeys.SEARCH_NO_RESULTS));
Expand All @@ -319,7 +332,7 @@ void performSearch(@NotNull Audience sender,

int offset = (page - 1) * PAGE_SIZE;
List<SearchResultEntity> results = mapper.search(includeFreehold, includeLeasehold,
tagIds, excludedTagIds, minPrice, maxPrice, PAGE_SIZE, offset);
tagIds, excludedTagIds, excludeRented, minPrice, maxPrice, PAGE_SIZE, offset);

TextComponent.Builder builder = Component.text();
builder.append(messages.messageFor(MessageKeys.SEARCH_HEADER,
Expand All @@ -336,7 +349,7 @@ void performSearch(@NotNull Audience sender,
}

appendFooter(builder, includeFreehold, includeLeasehold, tagIds, excludedTagIds,
minPrice, maxPrice, page, totalPages);
excludeRented, minPrice, maxPrice, page, totalPages);
sender.sendMessage(builder.build());
} catch (Exception ex) {
sender.sendMessage(messages.messageFor(MessageKeys.SEARCH_ERROR,
Expand All @@ -361,15 +374,16 @@ private void appendFooter(@NotNull TextComponent.Builder builder,
boolean includeFreehold, boolean includeLeasehold,
@Nullable Collection<String> tagIds,
@Nullable Collection<String> excludedTagIds,
boolean excludeRented,
double minPrice, double maxPrice,
int page, int totalPages) {
Component previousComponent = page > 1
? buildNavComponent(MessageKeys.SEARCH_PREVIOUS, includeFreehold, includeLeasehold,
tagIds, excludedTagIds, minPrice, maxPrice, page - 1)
tagIds, excludedTagIds, excludeRented, minPrice, maxPrice, page - 1)
: Component.empty();
Component nextComponent = page < totalPages
? buildNavComponent(MessageKeys.SEARCH_NEXT, includeFreehold, includeLeasehold,
tagIds, excludedTagIds, minPrice, maxPrice, page + 1)
tagIds, excludedTagIds, excludeRented, minPrice, maxPrice, page + 1)
: Component.empty();
builder.appendNewline()
.append(messages.messageFor(MessageKeys.SEARCH_FOOTER,
Expand All @@ -383,8 +397,21 @@ private void appendFooter(@NotNull TextComponent.Builder builder,
boolean includeFreehold, boolean includeLeasehold,
@Nullable Collection<String> tagIds,
@Nullable Collection<String> excludedTagIds,
boolean excludeRented,
double minPrice, double maxPrice,
int targetPage) {
return parseMiniMessage(key, "<command>", buildResultsCommand(includeFreehold, includeLeasehold,
tagIds, excludedTagIds, excludeRented, minPrice, maxPrice, targetPage));
}

static @NotNull String buildResultsCommand(boolean includeFreehold,
boolean includeLeasehold,
@Nullable Collection<String> tagIds,
@Nullable Collection<String> excludedTagIds,
boolean excludeRented,
double minPrice,
double maxPrice,
int targetPage) {
StringBuilder command = new StringBuilder("/realty search results");
if (includeFreehold) {
command.append(" --freehold");
Expand All @@ -398,14 +425,17 @@ private void appendFooter(@NotNull TextComponent.Builder builder,
if (excludedTagIds != null && !excludedTagIds.isEmpty()) {
command.append(" --exclude-tags ").append(String.join(",", excludedTagIds));
}
if (excludeRented) {
command.append(" --exclude-rented");
}
if (minPrice > 0) {
command.append(" --min-price ").append(minPrice);
}
if (maxPrice < Double.MAX_VALUE) {
command.append(" --max-price ").append(maxPrice);
}
command.append(" --page ").append(targetPage);
return parseMiniMessage(key, "<command>", command.toString());
return command.toString();
}

private @NotNull Component parseMiniMessage(@NotNull String key,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.github.md5sha256.realty.command;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.util.List;

class SearchDialogTest {

@Nested
@DisplayName("buildResultsCommand")
class BuildResultsCommand {

@Test
@DisplayName("includes exclude-rented flag when requested")
void includesExcludeRentedFlag() {
String command = SearchDialog.buildResultsCommand(
true, true, List.of("farm", "port"), List.of("vip"),
true, 25.0, 250.0, 3);

Assertions.assertTrue(command.contains(" --exclude-rented"));
Assertions.assertTrue(command.contains(" --freehold"));
Assertions.assertTrue(command.contains(" --leasehold"));
Assertions.assertTrue(command.contains(" --tags farm,port"));
Assertions.assertTrue(command.contains(" --exclude-tags vip"));
Assertions.assertTrue(command.endsWith(" --page 3"));
}

@Test
@DisplayName("omits exclude-rented flag by default")
void omitsExcludeRentedFlag() {
String command = SearchDialog.buildResultsCommand(
false, true, null, null,
false, 0.0, Double.MAX_VALUE, 1);

Assertions.assertFalse(command.contains(" --exclude-rented"));
Assertions.assertEquals("/realty search results --leasehold --page 1", command);
}
}
}