Single method for creating multiple entity types in Lua
This commit is contained in:
parent
944fc1cd46
commit
4c088d354b
2 changed files with 494 additions and 0 deletions
|
|
@ -23,6 +23,7 @@ import org.luaj.vm2.LuaFunction;
|
|||
import org.luaj.vm2.LuaTable;
|
||||
import org.luaj.vm2.LuaValue;
|
||||
|
||||
import games.stendhal.common.Direction;
|
||||
import games.stendhal.server.core.engine.GameEvent;
|
||||
import games.stendhal.server.core.engine.SingletonRepository;
|
||||
import games.stendhal.server.core.engine.StendhalRPZone;
|
||||
|
|
@ -32,6 +33,8 @@ import games.stendhal.server.core.pathfinder.Node;
|
|||
import games.stendhal.server.core.rp.StendhalRPAction;
|
||||
import games.stendhal.server.core.rule.EntityManager;
|
||||
import games.stendhal.server.core.scripting.ScriptInLua.LuaLogger;
|
||||
import games.stendhal.server.entity.CollisionAction;
|
||||
import games.stendhal.server.entity.Entity;
|
||||
import games.stendhal.server.entity.RPEntity;
|
||||
import games.stendhal.server.entity.creature.Creature;
|
||||
import games.stendhal.server.entity.creature.RaidCreature;
|
||||
|
|
@ -43,6 +46,7 @@ import games.stendhal.server.entity.mapstuff.sign.Sign;
|
|||
import games.stendhal.server.entity.npc.ChatAction;
|
||||
import games.stendhal.server.entity.npc.ChatCondition;
|
||||
import games.stendhal.server.entity.npc.ConversationStates;
|
||||
import games.stendhal.server.entity.npc.PassiveNPC;
|
||||
import games.stendhal.server.entity.npc.SilentNPC;
|
||||
import games.stendhal.server.entity.npc.SpeakerNPC;
|
||||
import games.stendhal.server.entity.player.Player;
|
||||
|
|
@ -59,6 +63,7 @@ public class LuaEntityHelper {
|
|||
|
||||
private static final LuaConditionHelper conditionHelper = LuaConditionHelper.get();
|
||||
private static final LuaActionHelper actionHelper = LuaActionHelper.get();
|
||||
private static final LuaTableHelper tableHelper = LuaTableHelper.get();
|
||||
|
||||
private static LuaEntityHelper instance;
|
||||
|
||||
|
|
@ -77,6 +82,441 @@ public class LuaEntityHelper {
|
|||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `Entity` instance.
|
||||
*
|
||||
* @param lt
|
||||
* Entity definition table.
|
||||
* @return
|
||||
* New `Entity` instance.
|
||||
*/
|
||||
public Entity create(final LuaTable lt) {
|
||||
lt.checktable();
|
||||
|
||||
String e_type = lt.get("type").tojstring();
|
||||
|
||||
// default to SilentNPC
|
||||
if (e_type == null) {
|
||||
e_type = "silentnpc";
|
||||
}
|
||||
|
||||
switch (e_type.toLowerCase()) {
|
||||
case "speakernpc":
|
||||
return buildSpeakerNPC(lt);
|
||||
case "silentnpc":
|
||||
return buildSilentNPC(lt);
|
||||
case "sign":
|
||||
return buildSign(lt);
|
||||
case "reader":
|
||||
return buildReader(lt);
|
||||
case "shopsign":
|
||||
return buildShopSign(lt);
|
||||
default:
|
||||
logger.error("unknown entity type: " + e_type);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds attributes defined in {@link games.stendhal.server.entity.Entity}.
|
||||
*
|
||||
* @param ent
|
||||
* The entity to whom attributes will be added.
|
||||
* @param lt
|
||||
* Lua table of attributes.
|
||||
*/
|
||||
private void setEntityTraits(final Entity ent, final LuaTable lt) {
|
||||
final LuaValue l_pos = lt.get("pos");
|
||||
if (!l_pos.isnil()) {
|
||||
ent.setPosition(l_pos.get(1).checkint(), l_pos.get(2).checkint());
|
||||
}
|
||||
|
||||
final LuaValue l_desc = lt.get("description");
|
||||
if (!l_desc.isnil()) {
|
||||
ent.setDescription(l_desc.checkjstring());
|
||||
}
|
||||
|
||||
final LuaValue l_class = lt.get("class");
|
||||
if (!l_class.isnil()) {
|
||||
ent.setEntityClass(l_class.checkjstring());
|
||||
}
|
||||
|
||||
final LuaValue l_subclass = lt.get("subclass");
|
||||
if (!l_subclass.isnil()) {
|
||||
ent.setEntitySubclass(l_subclass.checkjstring());
|
||||
}
|
||||
|
||||
final LuaValue l_resistance = lt.get("resistance");
|
||||
if (!l_resistance.isnil()) {
|
||||
ent.setResistance(l_resistance.checkint());
|
||||
}
|
||||
|
||||
final LuaValue l_size = lt.get("size");
|
||||
if (!l_size.isnil()) {
|
||||
ent.setSize(l_size.get(1).checkint(), l_size.get(2).checkint());
|
||||
}
|
||||
|
||||
final LuaValue l_cursor = lt.get("cursor");
|
||||
if (!l_cursor.isnil()) {
|
||||
ent.setCursor(l_cursor.checkjstring());
|
||||
}
|
||||
|
||||
final LuaValue l_visibility = lt.get("visibility");
|
||||
if (!l_visibility.isnil()) {
|
||||
ent.setVisibility(l_visibility.checkint());
|
||||
}
|
||||
|
||||
final LuaValue l_menu = lt.get("menu");
|
||||
if (!l_menu.isnil()) {
|
||||
ent.setMenu(l_menu.checkjstring());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds attributes defined in {@link games.stendhal.server.entity.npc.PassiveNPC}.
|
||||
*
|
||||
* @param npc
|
||||
* The entity to whom attributes will be added.
|
||||
* @param lt
|
||||
* Lua table of attributes.
|
||||
*/
|
||||
private void setNPCTraits(final PassiveNPC npc, final LuaTable lt) {
|
||||
|
||||
// *** Entity ***
|
||||
|
||||
setEntityTraits(npc, lt);
|
||||
|
||||
// *** ActiveEntity ***
|
||||
|
||||
final LuaValue l_dir = lt.get("dir");
|
||||
if (!l_dir.isnil()) {
|
||||
npc.setDirection((Direction) l_dir.checkuserdata(Direction.class));
|
||||
}
|
||||
|
||||
final LuaValue l_ignoresCollision = lt.get("ignoresCollision");
|
||||
if (!l_ignoresCollision.isnil()) {
|
||||
npc.setIgnoresCollision(l_ignoresCollision.checkboolean());
|
||||
}
|
||||
|
||||
// *** GuidedEntity ***
|
||||
|
||||
LuaValue l_pos = lt.get("pos"); // Entity.class
|
||||
final LuaValue l_path = lt.get("path");
|
||||
|
||||
// entity starts at first node of path
|
||||
if (l_pos.isnil()) {
|
||||
l_pos = l_path.get(1);
|
||||
}
|
||||
|
||||
if (!l_pos.isnil()) {
|
||||
l_pos.checktable();
|
||||
npc.setPosition(l_pos.get(1).checkint(), l_pos.get(2).checkint());
|
||||
}
|
||||
if (!l_path.isnil()) {
|
||||
l_path.checktable();
|
||||
|
||||
boolean loop = true;
|
||||
final LuaValue l_loop = l_path.get("loop");
|
||||
if (!l_loop.isnil()) {
|
||||
loop = l_loop.checkboolean();
|
||||
}
|
||||
|
||||
npc.setPath(tableToPath((LuaTable) l_path.get("nodes"), loop));
|
||||
/*
|
||||
npc.setPath(tableHelper.pairsToNodes(
|
||||
new FixedPath(l_path.get("nodes").checktable())), loop);
|
||||
*/
|
||||
|
||||
final LuaValue l_retrace = l_path.get("retrace");
|
||||
if (!l_retrace.isnil() && l_retrace.checkboolean()) {
|
||||
npc.setRetracePath();
|
||||
}
|
||||
|
||||
final LuaValue l_collisionAction = l_path.get("collisionAction");
|
||||
if (!l_collisionAction.isnil()) {
|
||||
npc.setCollisionAction((CollisionAction) l_collisionAction.checkuserdata(CollisionAction.class));
|
||||
}
|
||||
|
||||
// TODO: GuidedEntity.addSuspend()
|
||||
}
|
||||
|
||||
/*
|
||||
final LuaValue l_randMovement = lt.get("randomMovement");
|
||||
if (!l_randMovement.isnil()) {
|
||||
l_randMovement.checktable();
|
||||
|
||||
Boolean ret;
|
||||
final LuaValue l_ret = l_randMovement.get("ret");
|
||||
if (!l_ret.isnil()) {
|
||||
ret = l_ret.checkboolean();
|
||||
}
|
||||
|
||||
if (ret != null) {
|
||||
npc.setMovementRadius(l_randMovement.get("radius").checkint(), ret);
|
||||
} else {
|
||||
npc.setMovementRadius(l_randMovement.get("radius").checkint());
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
final LuaValue l_speed = lt.get("speed");
|
||||
if (!l_speed.isnil()) {
|
||||
npc.setBaseSpeed(l_speed.checkdouble());
|
||||
}
|
||||
|
||||
// *** RPEntity ***
|
||||
|
||||
final LuaValue l_basehp = lt.get("basehp");
|
||||
if (!l_basehp.isnil()) {
|
||||
npc.setBaseHP(l_basehp.checkint());
|
||||
}
|
||||
|
||||
final LuaValue l_hp = lt.get("hp");
|
||||
if (!l_hp.isnil()) {
|
||||
npc.setHP(l_hp.checkint());
|
||||
}
|
||||
|
||||
final LuaValue l_title = lt.get("title");
|
||||
if (!l_title.isnil()) {
|
||||
npc.setTitle(l_title.checkjstring());
|
||||
}
|
||||
|
||||
// *** DressedEntity ***
|
||||
|
||||
final LuaValue l_outfit = lt.get("outfit");
|
||||
if (!l_outfit.isnil()) {
|
||||
npc.setOutfit(l_outfit.get("layers").checkjstring());
|
||||
|
||||
final LuaTable l_outfitColors = (LuaTable) l_outfit.get("colors");
|
||||
if (!l_outfitColors.isnil()) {
|
||||
l_outfitColors.checktable();
|
||||
|
||||
for (final LuaValue l_key: l_outfitColors.keys()) {
|
||||
npc.setOutfitColor(l_key.checkjstring(), l_outfitColors.get(l_key).checkint());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// *** NPC ***
|
||||
|
||||
final LuaValue l_idea = lt.get("idea");
|
||||
if (!l_idea.isnil()) {
|
||||
npc.setIdea(l_idea.checkjstring());
|
||||
}
|
||||
|
||||
final LuaValue l_sounds = lt.get("sounds");
|
||||
if (l_sounds.istable() && l_sounds.length() > 0) {
|
||||
final List<String> sounds = new ArrayList<>();
|
||||
for (int idx = 1; idx <= l_sounds.length(); idx++) {
|
||||
sounds.add(l_sounds.get(idx).checkjstring());
|
||||
}
|
||||
|
||||
npc.setSounds(sounds);
|
||||
}
|
||||
|
||||
// TODO: NPC: setMovement, setRandomPathFrom, setPerceptionRange, setMovementRange
|
||||
// moveRandomly, setPathCompletedPause
|
||||
|
||||
// *** PassiveNPC ***
|
||||
|
||||
final LuaValue l_teleports = lt.get("teleports");
|
||||
if (!l_teleports.isnil()) {
|
||||
npc.setTeleportsFlag(l_teleports.checkboolean());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new interactive NPC.
|
||||
*
|
||||
* @param lt
|
||||
* Lua table of attributes.
|
||||
* @see
|
||||
* games.stendhal.server.entity.npc.SpeakerNPC.
|
||||
*/
|
||||
private LuaSpeakerNPC buildSpeakerNPC(final LuaTable lt) {
|
||||
final LuaSpeakerNPC npc = new LuaSpeakerNPC(lt.get("name").checkjstring());
|
||||
setNPCTraits(npc, lt);
|
||||
|
||||
LuaValue l_idleDir = lt.get("idleDir");
|
||||
if (l_idleDir.isnil()) {
|
||||
l_idleDir = lt.get("idleDirection");
|
||||
}
|
||||
if (!l_idleDir.isnil()) {
|
||||
npc.setIdleDirection((Direction) l_idleDir.checkuserdata(Direction.class));
|
||||
}
|
||||
|
||||
final LuaValue l_chatTimeout = lt.get("chatTimeout");
|
||||
if (!l_chatTimeout.isnil()) {
|
||||
npc.setPlayerChatTimeout(l_chatTimeout.checklong());
|
||||
}
|
||||
|
||||
final LuaValue l_perceptionRange = lt.get("perceptionRange");
|
||||
if (!l_perceptionRange.isnil()) {
|
||||
npc.setPerceptionRange(l_perceptionRange.checkint());
|
||||
}
|
||||
|
||||
final LuaValue l_currentState = lt.get("currentState");
|
||||
if (!l_currentState.isnil()) {
|
||||
npc.setCurrentState((ConversationStates) l_currentState.checkuserdata(ConversationStates.class));
|
||||
}
|
||||
|
||||
final LuaValue l_greeting = lt.get("greeting");
|
||||
if (!l_greeting.isnil()) {
|
||||
l_greeting.checktable();
|
||||
|
||||
final String greetText = l_greeting.get("text").checkjstring();
|
||||
final LuaValue l_greetAction = l_greeting.get("action");
|
||||
if (!l_greetAction.isnil()) {
|
||||
npc.addGreeting(greetText, (ChatAction) l_greetAction.checkuserdata(ChatAction.class));
|
||||
} else {
|
||||
npc.addGreeting(greetText);
|
||||
}
|
||||
}
|
||||
|
||||
final LuaValue l_replies = lt.get("replies");
|
||||
if (!l_replies.isnil()) {
|
||||
l_replies.checktable();
|
||||
|
||||
final LuaValue l_replyQuest = l_replies.get("quest");
|
||||
if (!l_replyQuest.isnil()) {
|
||||
npc.addQuest(l_replyQuest.checkjstring());
|
||||
}
|
||||
|
||||
final LuaValue l_replyJob = l_replies.get("job");
|
||||
if (!l_replyJob.isnil()) {
|
||||
npc.addJob(l_replyJob.checkjstring());
|
||||
}
|
||||
|
||||
final LuaValue l_replyHelp = l_replies.get("help");
|
||||
if (!l_replyHelp.isnil()) {
|
||||
npc.addHelp(l_replyHelp.checkjstring());
|
||||
}
|
||||
|
||||
final LuaValue l_replyOffer = l_replies.get("offer");
|
||||
if (!l_replyOffer.isnil()) {
|
||||
npc.addOffer(l_replyOffer.checkjstring());
|
||||
}
|
||||
|
||||
final LuaValue l_replyBye = l_replies.get("bye");
|
||||
if (!l_replyBye.isnil()) {
|
||||
npc.addGoodbye(l_replyBye.checkjstring());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: addReply
|
||||
|
||||
final LuaValue l_altImage = lt.get("alternativeImage");
|
||||
if (!l_altImage.isnil()) {
|
||||
npc.setAlternativeImage(l_altImage.checkjstring());
|
||||
}
|
||||
|
||||
return npc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new non-interactive NPC.
|
||||
*
|
||||
* @param lt
|
||||
* Lua table of attributes.
|
||||
* @see
|
||||
* games.stendhal.server.entity.npc.SilentNPC
|
||||
*/
|
||||
private LuaSilentNPC buildSilentNPC(final LuaTable lt) {
|
||||
final LuaSilentNPC npc = new LuaSilentNPC() {};
|
||||
setNPCTraits(npc, lt);
|
||||
|
||||
return npc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sign.
|
||||
*
|
||||
* @param lt
|
||||
* Lua table of attributes.
|
||||
* @param visible
|
||||
* Whether or not the sign should us a sprite visible to the player.
|
||||
* @see
|
||||
* games.stendhal.server.entity.mapstuff.sign.Sign
|
||||
* @see
|
||||
* games.stendhal.server.entity.mapstuff.sign.Reader
|
||||
*/
|
||||
private Sign buildSign(final LuaTable lt, final boolean visible) {
|
||||
if (!visible) {
|
||||
return buildReader(lt);
|
||||
}
|
||||
|
||||
final Sign sign = new Sign();
|
||||
setEntityTraits(sign, lt);
|
||||
|
||||
final LuaValue l_text = lt.get("text");
|
||||
if (!l_text.isnil()) {
|
||||
sign.setText(l_text.checkjstring());
|
||||
}
|
||||
|
||||
return sign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new sign.
|
||||
*
|
||||
* @param lt
|
||||
* Lua table of attributes.
|
||||
* @see
|
||||
* {@link #buildSign(LuaTable, boolean)}
|
||||
*/
|
||||
private Sign buildSign(final LuaTable lt) {
|
||||
boolean visible = true;
|
||||
final LuaValue l_value = lt.get("visible");
|
||||
if (!l_value.isnil()) {
|
||||
visible = l_value.toboolean();
|
||||
}
|
||||
|
||||
return buildSign(lt, visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new invisible sign.
|
||||
*
|
||||
* @param lt
|
||||
* Lua table of attributes.
|
||||
* @see
|
||||
* games.stendhal.server.entity.mapstuff.sign.Sign
|
||||
* @see
|
||||
* games.stendhal.server.entity.mapstuff.sign.Reader
|
||||
*/
|
||||
private Reader buildReader(final LuaTable lt) {
|
||||
final Reader reader = new Reader();
|
||||
setEntityTraits(reader, lt);
|
||||
|
||||
final LuaValue l_text = lt.get("text");
|
||||
if (!l_text.isnil()) {
|
||||
reader.setText(l_text.checkjstring());
|
||||
}
|
||||
|
||||
return reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new shop sign.
|
||||
*
|
||||
* @param lt
|
||||
* Lua table of attributes.
|
||||
* @see
|
||||
* games.stendhal.server.entity.mapstuff.sign.ShopSign
|
||||
*/
|
||||
private ShopSign buildShopSign(final LuaTable lt) {
|
||||
final ShopSign sign = new ShopSign(
|
||||
lt.get("name").checkjstring(),
|
||||
lt.get("title").checkjstring(),
|
||||
lt.get("caption").checkjstring(),
|
||||
lt.get("seller").checkboolean());
|
||||
|
||||
setEntityTraits(sign, lt);
|
||||
|
||||
return sign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a table of coordinates to a FixedPath instance.
|
||||
*
|
||||
|
|
@ -195,7 +635,10 @@ public class LuaEntityHelper {
|
|||
* Name of new NPC.
|
||||
* @return
|
||||
* New SpeakerNPC instance.
|
||||
* @deprecated
|
||||
* Use {@link #create(LuaTable)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public LuaSpeakerNPC createSpeakerNPC(final String name) {
|
||||
return new LuaSpeakerNPC(name);
|
||||
}
|
||||
|
|
@ -205,7 +648,10 @@ public class LuaEntityHelper {
|
|||
*
|
||||
* @return
|
||||
* New SilentNPC instance.
|
||||
* @deprecated
|
||||
* Use {@link #create(LuaTable)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public LuaSilentNPC createSilentNPC() {
|
||||
return new LuaSilentNPC();
|
||||
}
|
||||
|
|
@ -289,7 +735,10 @@ public class LuaEntityHelper {
|
|||
*
|
||||
* @return
|
||||
* New Sign instance.
|
||||
* @deprecated
|
||||
* Use {@link #create(LuaTable)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public Sign createSign() {
|
||||
return createSign(true);
|
||||
}
|
||||
|
|
@ -301,7 +750,10 @@ public class LuaEntityHelper {
|
|||
* If <code>false</code>, sign does not have a visual representation.
|
||||
* @return
|
||||
* New Sign instance.
|
||||
* @deprecated
|
||||
* Use {@link #create(LuaTable)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public Sign createSign(final boolean visible) {
|
||||
if (visible) {
|
||||
return new Sign();
|
||||
|
|
@ -323,7 +775,10 @@ public class LuaEntityHelper {
|
|||
* <code>true</code>, if this sign is for items sold by an NPC (defaults to <code>true</code> if <code>null</code>).
|
||||
* @return
|
||||
* New ShopSign instance.
|
||||
* @deprecated
|
||||
* Use {@link #create(LuaTable)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public ShopSign createShopSign(final String name, final String title, final String caption, Boolean seller) {
|
||||
// default to seller
|
||||
if (seller == null) {
|
||||
|
|
@ -408,6 +863,10 @@ public class LuaEntityHelper {
|
|||
/**
|
||||
* A special interface that overloads setPath & setPathAndPosition
|
||||
* methods to accept a Lua table as parameter argument.
|
||||
*
|
||||
* @todo
|
||||
* Merge functionality into {@link games.stendhal.server.entity.GuidedEntity}
|
||||
* & delete this class.
|
||||
*/
|
||||
private interface LuaGuidedEntity {
|
||||
|
||||
|
|
@ -420,6 +879,11 @@ public class LuaEntityHelper {
|
|||
public void setPathAndPosition(final LuaTable table, Boolean loop);
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo
|
||||
* Merge functionality into {@link games.stendhal.server.entity.npc.SpeakerNPC}
|
||||
* & delete this class.
|
||||
*/
|
||||
private class LuaSpeakerNPC extends SpeakerNPC implements LuaGuidedEntity {
|
||||
|
||||
private LuaFunction goodbyeAction;
|
||||
|
|
@ -567,6 +1031,11 @@ public class LuaEntityHelper {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo
|
||||
* Merge functionality into {@link games.stendhal.server.entity.npc.SilentNPC}
|
||||
* & delete this class.
|
||||
*/
|
||||
private class LuaSilentNPC extends SilentNPC implements LuaGuidedEntity {
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import org.luaj.vm2.LuaTable;
|
|||
import org.luaj.vm2.LuaValue;
|
||||
import org.luaj.vm2.lib.jse.CoerceJavaToLua;
|
||||
|
||||
import games.stendhal.server.core.pathfinder.Node;
|
||||
|
||||
|
||||
/**
|
||||
* Adds some useful function members to Lua "table" object.
|
||||
|
|
@ -96,4 +98,27 @@ public class LuaTableHelper {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Lua table pair ({num, num}) to `Node`.
|
||||
*/
|
||||
public static Node pairToNode(final LuaTable lt) {
|
||||
lt.checktable();
|
||||
return new Node(lt.get(1).checkint(), lt.get(2).checkint());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Lua table of table pairs ({{int, int}, {int, int}}) to
|
||||
* list of nodes (`List<Node>`).
|
||||
*/
|
||||
public static List<Node> pairsToNodes(final LuaTable lt) {
|
||||
lt.checktable();
|
||||
final List<Node> nodes = new LinkedList<>();
|
||||
|
||||
for (int idx=1; idx <= lt.length(); idx++) {
|
||||
nodes.add(pairToNode((LuaTable) lt.get(idx)));
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue