|
| 1 | +package net.neoforged.camelot.api.config.impl; |
| 2 | + |
| 3 | +import net.dv8tion.jda.api.components.buttons.Button; |
| 4 | +import net.dv8tion.jda.api.components.container.Container; |
| 5 | +import net.dv8tion.jda.api.components.container.ContainerChildComponent; |
| 6 | +import net.dv8tion.jda.api.components.section.Section; |
| 7 | +import net.dv8tion.jda.api.components.textdisplay.TextDisplay; |
| 8 | +import net.dv8tion.jda.api.entities.Message; |
| 9 | +import net.dv8tion.jda.api.utils.messages.MessageCreateData; |
| 10 | +import net.dv8tion.jda.api.utils.messages.MessageEditBuilder; |
| 11 | +import net.dv8tion.jda.api.utils.messages.MessageEditData; |
| 12 | +import net.neoforged.camelot.api.config.ConfigManager; |
| 13 | +import net.neoforged.camelot.api.config.type.OptionBuilder; |
| 14 | +import net.neoforged.camelot.api.config.type.OptionBuilderFactory; |
| 15 | +import net.neoforged.camelot.api.config.type.OptionType; |
| 16 | +import org.jetbrains.annotations.Nullable; |
| 17 | +import org.json.JSONException; |
| 18 | +import org.json.JSONObject; |
| 19 | + |
| 20 | +import java.util.ArrayList; |
| 21 | +import java.util.List; |
| 22 | +import java.util.function.Function; |
| 23 | +import java.util.stream.Collectors; |
| 24 | + |
| 25 | +final class ObjectOption<O> implements OptionType<O> { |
| 26 | + private final List<OptionInfo<O, ?>> options; |
| 27 | + private final Creator<O> creator; |
| 28 | + @Nullable |
| 29 | + private final Function<O, String> formatter; |
| 30 | + |
| 31 | + ObjectOption(List<OptionInfo<O, ?>> options, Creator<O> creator, @Nullable Function<O, String> formatter) { |
| 32 | + this.options = options; |
| 33 | + this.creator = creator; |
| 34 | + this.formatter = formatter; |
| 35 | + } |
| 36 | + |
| 37 | + @Override |
| 38 | + public String serialise(O value) { |
| 39 | + var o = new JSONObject(); |
| 40 | + for (OptionInfo<O, ?> option : options) { |
| 41 | + o.put(option.id(), option.serialise(value)); |
| 42 | + } |
| 43 | + return o.toString(); |
| 44 | + } |
| 45 | + |
| 46 | + @Override |
| 47 | + public O deserialize(String value) { |
| 48 | + var obj = new JSONObject(value); |
| 49 | + return creator.create(options.stream() |
| 50 | + .map(i -> { |
| 51 | + try { |
| 52 | + return i.type().deserialize(obj.getString(i.id())); |
| 53 | + } catch (JSONException ex) { |
| 54 | + return i.defaultValue(); |
| 55 | + } |
| 56 | + }) |
| 57 | + .toList()); |
| 58 | + } |
| 59 | + |
| 60 | + @Override |
| 61 | + public Button createUpdateButton(O currentValue, Function<O, MessageEditData> updater, ComponentCreator components) { |
| 62 | + return components.button(event -> event.reply(MessageCreateData.fromEditData(objectView(event.getMessage(), currentValue, updater, components))) |
| 63 | + .setEphemeral(true).queue()).withLabel("Modify"); |
| 64 | + } |
| 65 | + |
| 66 | + private MessageEditData objectView(Message topLevelMessage, O currentValue, Function<O, MessageEditData> updater, ComponentCreator components) { |
| 67 | + var msgComponents = new ArrayList<ContainerChildComponent>(); |
| 68 | + msgComponents.add(TextDisplay.of("## Configure object")); |
| 69 | + for (int i = 0; i < options.size(); i++) { |
| 70 | + msgComponents.add(section(topLevelMessage, i, options.get(i), currentValue, updater, components)); |
| 71 | + } |
| 72 | + return new MessageEditBuilder() |
| 73 | + .setComponents(List.of(Container.of(msgComponents))) |
| 74 | + .useComponentsV2() |
| 75 | + .build(); |
| 76 | + } |
| 77 | + |
| 78 | + private <T> Section section(Message topLevelMessage, int idx, OptionInfo<O, T> info, O value, Function<O, MessageEditData> updater, ComponentCreator components) { |
| 79 | + var current = value == null ? info.defaultValue : info.extractor().apply(value); |
| 80 | + return Section.of( |
| 81 | + info.type().createUpdateButton( |
| 82 | + current, |
| 83 | + newValue -> { |
| 84 | + var newObject = newObject(value, idx, newValue); |
| 85 | + topLevelMessage.editMessage(updater.apply(newObject)).queue(); |
| 86 | + return objectView(topLevelMessage, newObject, updater, components); |
| 87 | + }, |
| 88 | + components |
| 89 | + ), |
| 90 | + TextDisplay.ofFormat( |
| 91 | + "**" + info.displayName() + "**\n" |
| 92 | + + info.desc() + "\n\n" |
| 93 | + + "Curent value: " + (current == null ? "*none*" : info.type().formatFullPageView(current)) |
| 94 | + ) |
| 95 | + ); |
| 96 | + } |
| 97 | + |
| 98 | + private O newObject(O current, int index, Object newValue) { |
| 99 | + var newObjects = new ArrayList<>(options.size()); |
| 100 | + for (var option : options) { |
| 101 | + newObjects.add(current == null ? option.defaultValue : option.extractor().apply(current)); |
| 102 | + } |
| 103 | + newObjects.set(index, newValue); |
| 104 | + return creator.create(newObjects); |
| 105 | + } |
| 106 | + |
| 107 | + @Override |
| 108 | + public String format(O value) { |
| 109 | + if (formatter != null) return formatter.apply(value); |
| 110 | + return options.stream().map(o -> o.displayName() + ": " + o.format(value)).collect(Collectors.joining(", ")); |
| 111 | + } |
| 112 | + |
| 113 | + private record OptionInfo<O, T>(String id, String displayName, String desc, OptionType<T> type, T defaultValue, |
| 114 | + Function<O, T> extractor) { |
| 115 | + private String format(O value) { |
| 116 | + return type().format(extractor().apply(value)); |
| 117 | + } |
| 118 | + |
| 119 | + private String serialise(O value) { |
| 120 | + return type.serialise(extractor.apply(value)); |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + static class Builder<G, O> extends OptionBuilderImpl<G, O, OptionBuilder.Composite<G, O>> implements OptionBuilder.Composite<G, O> { |
| 125 | + private final java.util.List<BuilderOption<G, O, ?, ?>> options; |
| 126 | + private final Creator<O> creator; |
| 127 | + |
| 128 | + private Function<O, String> formatter; |
| 129 | + |
| 130 | + protected Builder(ConfigManager<G> manager, String path, String id, java.util.List<BuilderOption<G, O, ?, ?>> options, Creator<O> creator) { |
| 131 | + super(manager, path, id); |
| 132 | + this.options = options; |
| 133 | + this.creator = creator; |
| 134 | + } |
| 135 | + |
| 136 | + @Override |
| 137 | + @SuppressWarnings({"rawtypes", "unchecked"}) |
| 138 | + protected OptionType<O> createType() { |
| 139 | + return new ObjectOption( |
| 140 | + options.stream() |
| 141 | + .map(o -> { |
| 142 | + var b = (OptionBuilderImpl) o.builder(manager); |
| 143 | + return new OptionInfo( |
| 144 | + b.id, b.name, b.description, b.createType(), b.defaultValue, o.extractor |
| 145 | + ); |
| 146 | + }) |
| 147 | + .toList(), |
| 148 | + creator, |
| 149 | + formatter |
| 150 | + ); |
| 151 | + } |
| 152 | + |
| 153 | + @Override |
| 154 | + public Composite<G, O> formatter(Function<O, String> formatter) { |
| 155 | + this.formatter = formatter; |
| 156 | + return this; |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + record Factory<G, T>(List<BuilderOption<G, T, ?, ?>> options, |
| 161 | + Creator<T> creator) implements OptionBuilderFactory<G, T, OptionBuilder.Composite<G, T>> { |
| 162 | + @Override |
| 163 | + public Builder<G, T> create(ConfigManager<G> manager, String path, String id) { |
| 164 | + return new Builder<>(manager, path, id, options, creator); |
| 165 | + } |
| 166 | + } |
| 167 | + |
| 168 | + record BuilderOption<G, O, F, Z>(String id, Function<O, Z> extractor, OptionBuilderFactory<G, F, ?> factory, |
| 169 | + Function<OptionBuilder<G, F, ?>, OptionBuilder<G, Z, ?>> configurator) { |
| 170 | + public OptionBuilder<G, Z, ?> builder(ConfigManager<G> manager) { |
| 171 | + var builder = factory.create(manager, "_", this.id); |
| 172 | + if (builder instanceof OptionBuilderImpl<G, F, ? extends OptionBuilder<G, F, ?>> obi) { |
| 173 | + builder = obi.setCannotBeRegistered(); |
| 174 | + } |
| 175 | + return configurator.apply(builder); |
| 176 | + } |
| 177 | + } |
| 178 | + |
| 179 | + @FunctionalInterface |
| 180 | + interface Creator<T> { |
| 181 | + T create(List<Object> args); |
| 182 | + } |
| 183 | +} |
0 commit comments