New source found from dndbeyond.com
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { all, call, put, select } from 'redux-saga/effects';
|
||||
import { ItemPlanAccessors } from "../../engine/ItemPlan";
|
||||
import { characterActions, characterEnvActions, featureFlagInfoActions, ruleDataActions, serviceDataActions, } from '../../actions';
|
||||
import { ApiRequests } from '../../api';
|
||||
import * as ApiUtils from '../../api/utils';
|
||||
import * as ApiAdapterUtils from '../../apiAdapter/utils';
|
||||
import { ConfigUtils } from '../../config';
|
||||
import { BackgroundAccessors } from '../../engine/Background';
|
||||
import { CampaignAccessors, PartyInventorySharingStateEnum } from '../../engine/Campaign';
|
||||
import { CampaignAccessors } from '../../engine/Campaign';
|
||||
import { CharacterDerivers } from '../../engine/Character';
|
||||
import { ChoiceAccessors } from '../../engine/Choice';
|
||||
import { ClassAccessors, ClassUtils } from '../../engine/Class';
|
||||
@@ -426,6 +427,16 @@ export function* handlePreferenceChoose(action) {
|
||||
[key]: value,
|
||||
});
|
||||
yield put(callCommitAction(characterActions.preferenceSet, key, value));
|
||||
// These cases need to happen after the preference is set in state
|
||||
switch (key) {
|
||||
case 'longRestType': {
|
||||
const data = yield call(SagaHelpers.getApiRequestData, ApiRequests.getCharacterRestLong);
|
||||
yield put(serviceDataActions.longRestTextSet(data));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
//not implemented
|
||||
}
|
||||
yield call(handleDataUpdates, data);
|
||||
}
|
||||
/**
|
||||
@@ -557,6 +568,7 @@ export function* handleClassRemoveRequest(action) {
|
||||
yield call(handleDataUpdates, data);
|
||||
yield call(autoUpdateInfusions);
|
||||
yield call(autoUpdateChoices);
|
||||
yield call(autoUpdateReplicatedItems);
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -858,7 +870,7 @@ export function* handleItemCustomizationsDelete(action) {
|
||||
action.meta.accept();
|
||||
}
|
||||
}
|
||||
export function* handleContainerDestroy(item, removeContents) {
|
||||
export function* handleContainerDestroy(item, removeContents, onAccept) {
|
||||
const itemMappingId = ItemAccessors.getMappingId(item);
|
||||
const containerLookup = yield select(rulesEngineSelectors.getContainerLookup);
|
||||
const inventoryLookup = yield select(rulesEngineSelectors.getInventoryLookup);
|
||||
@@ -920,6 +932,9 @@ export function* handleContainerDestroy(item, removeContents) {
|
||||
yield put(characterActions.itemRemove(itemMappingId, removeContents));
|
||||
}
|
||||
}
|
||||
if (onAccept) {
|
||||
onAccept();
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -933,7 +948,7 @@ export function* handleItemDestroy(action) {
|
||||
if (item) {
|
||||
//if item isContainer, handle removing container and contents
|
||||
if (ItemAccessors.isContainer(item)) {
|
||||
yield call(handleContainerDestroy, item, removeContainerContents);
|
||||
yield call(handleContainerDestroy, item, removeContainerContents, action.meta.accept);
|
||||
}
|
||||
else {
|
||||
//item is not container, just remove it (removeContainerContents should be false for now)
|
||||
@@ -951,20 +966,11 @@ export function* handleItemDestroy(action) {
|
||||
//if item isContainer, handle removing container and contents
|
||||
// TODO IMS: check that this works with shared containers
|
||||
if (ItemAccessors.isContainer(item)) {
|
||||
yield call(handleContainerDestroy, item, removeContainerContents);
|
||||
yield call(handleContainerDestroy, item, removeContainerContents, action.meta.accept);
|
||||
}
|
||||
else {
|
||||
//item is not container, just remove it (removeContainerContents should be false for now)
|
||||
yield put(serviceDataActions.partyItemRemove(id, false));
|
||||
const partyInfo = yield select(serviceDataSelectors.getPartyInfo);
|
||||
if (partyInfo &&
|
||||
CampaignAccessors.getSharingState(partyInfo) === PartyInventorySharingStateEnum.DELETE_ONLY) {
|
||||
const partyInventory = CampaignAccessors.getPartyBaseInventoryContracts(partyInfo);
|
||||
// If all that is left if what we just deleted
|
||||
if (partyInventory.length === 1 && ItemAccessors.getId(partyInventory[0]) === id) {
|
||||
yield put(serviceDataActions.partyCampaignInfoSet(Object.assign(Object.assign({}, partyInfo), { sharingState: PartyInventorySharingStateEnum.OFF })));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (action.meta.accept) {
|
||||
action.meta.accept();
|
||||
@@ -1027,6 +1033,7 @@ export function* handleItemMove(action) {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const partyInfo = yield select(serviceDataSelectors.getPartyInfo);
|
||||
const isCurrentContainerShared = ItemUtils.isShared(item, containerLookup);
|
||||
const isDestinationContainerShared = ContainerValidators.validateIsShared(DefinitionHacks.hack__generateDefinitionKey(action.payload.containerEntityTypeId, action.payload.containerEntityId), containerLookup);
|
||||
const isItemContainer = ItemAccessors.isContainer(item);
|
||||
@@ -1041,12 +1048,30 @@ export function* handleItemMove(action) {
|
||||
if (isItemContainer) {
|
||||
const inventoryItems = yield select(rulesEngineSelectors.getPartyInventory);
|
||||
const itemContainer = HelperUtils.lookupDataOrFallback(containerLookup, DefinitionHacks.hack__generateDefinitionKey(EntityTypeEnum.ITEM, ItemAccessors.getMappingId(item)));
|
||||
const currentCharacterId = yield select(rulesEngineSelectors.getId);
|
||||
// If item is container we need to move contents to inventory
|
||||
if (itemContainer) {
|
||||
const inventoryInContainer = ContainerUtils.getInventoryItems(itemContainer, inventoryItems);
|
||||
//loop through items in the container and move contents. If there is an infusion, only move if infusion belongs to the character otherwise leave in party inventory
|
||||
for (let i = 0; i < inventoryInContainer.length; i++) {
|
||||
yield put(callCommitAction(characterActions.itemAdd, Object.assign(Object.assign({}, inventoryInContainer[i]), { containerEntityId: ContainerAccessors.getMappingId(itemContainer), containerEntityTypeId: ContainerAccessors.getContainerType(itemContainer), containerDefinitionKey: ContainerAccessors.getDefinitionKey(itemContainer) })));
|
||||
yield put(callCommitAction(serviceDataActions.partyItemRemove, ItemAccessors.getMappingId(inventoryInContainer[i])));
|
||||
const infusion = ItemAccessors.getInfusion(inventoryInContainer[i]);
|
||||
let canMoveInfusion = false;
|
||||
if (infusion) {
|
||||
const infusionCharacterId = InfusionAccessors.getCharacterId(infusion);
|
||||
canMoveInfusion = infusionCharacterId === currentCharacterId;
|
||||
}
|
||||
const itemPartyRestrictions = ItemAccessors.getPartyInventoryRestrictions(inventoryInContainer[i]);
|
||||
const canMoveRestrictedItem = itemPartyRestrictions.length > 0
|
||||
? itemPartyRestrictions.some((restriction) => restriction.characterId === currentCharacterId)
|
||||
: true;
|
||||
if (partyInfo && (!canMoveRestrictedItem || (infusion && !canMoveInfusion))) {
|
||||
//if we can't move the infusion or replicated item, we need to remove the item from the container so it doesn't get "lost" when we move the container - leave the item in the party equipment container
|
||||
yield put(callCommitAction(serviceDataActions.partyItemMoveSet, ItemAccessors.getMappingId(inventoryInContainer[i]), CampaignAccessors.getId(partyInfo), ContainerTypeEnum.CAMPAIGN));
|
||||
}
|
||||
else {
|
||||
yield put(callCommitAction(characterActions.itemAdd, Object.assign(Object.assign({}, inventoryInContainer[i]), { containerEntityId: ContainerAccessors.getMappingId(itemContainer), containerEntityTypeId: ContainerAccessors.getContainerType(itemContainer), containerDefinitionKey: ContainerAccessors.getDefinitionKey(itemContainer) })));
|
||||
yield put(callCommitAction(serviceDataActions.partyItemRemove, ItemAccessors.getMappingId(inventoryInContainer[i])));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1381,6 +1406,19 @@ export function* handleClassFeatureChoiceSetRequest(action) {
|
||||
}
|
||||
//remove any spells that are associated with the spell lists from the subclass or feat choices
|
||||
yield call(apiRemoveSpellsBySpellListIds, spellListIdsToRemove);
|
||||
//remove any item mappings from feature options that come from item plans (magic item mappings)
|
||||
if (choiceType === BuilderChoiceTypeEnum.FEATURE_OPTION && oldCharClass) {
|
||||
const itemPlans = yield select(rulesEngineSelectors.getAvailableItemPlans);
|
||||
const chosenItemPlan = itemPlans.find((plan) => {
|
||||
const planChoice = ItemPlanAccessors.getChoice(plan);
|
||||
return ChoiceAccessors.getId(planChoice) === choiceId;
|
||||
});
|
||||
const item = chosenItemPlan ? ItemPlanAccessors.getItem(chosenItemPlan) : null;
|
||||
if (item) {
|
||||
// TODO: PARTY - can we send the party inventory update request here instead of from the component?
|
||||
yield put(characterActions.itemDestroy(ItemAccessors.getMappingId(item), false, action.meta.accept));
|
||||
}
|
||||
}
|
||||
yield call(apiClassFeatureChoiceSet, action);
|
||||
const classes = yield select(rulesEngineSelectors.getClasses);
|
||||
const charClass = classes.find((charClass) => !!oldCharClass && ClassAccessors.getId(charClass) === ClassAccessors.getId(oldCharClass));
|
||||
@@ -1502,6 +1540,7 @@ export function* handleClassLevelSetRequest(action) {
|
||||
yield call(autoUpdateInfusions);
|
||||
yield call(autoUpdateChoices);
|
||||
yield call(autoUpdateClassAlwaysPreparedSpells, [action.payload.classId]);
|
||||
yield call(autoUpdateReplicatedItems);
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -1731,6 +1770,27 @@ function* autoUpdateChoices(lastChoice = null) {
|
||||
yield call(autoUpdateBackgroundChoices);
|
||||
yield call(autoUpdateExpertiseChoices);
|
||||
}
|
||||
function* autoUpdateReplicatedItems() {
|
||||
const itemOriginLookup = yield select(rulesEngineSelectors.getAllInventoryLookupByOrigin);
|
||||
const itemPlans = yield select(rulesEngineSelectors.getAvailableItemPlans);
|
||||
const itemMappingIdsToRemove = [];
|
||||
Object.keys(itemOriginLookup).forEach((originKey) => {
|
||||
if (!itemPlans.some((itemPlan) => ItemPlanAccessors.getOriginDefinitionKey(itemPlan) === originKey)) {
|
||||
const item = itemOriginLookup[originKey];
|
||||
if (item) {
|
||||
// If the item is not part of any item plan, we can remove it
|
||||
itemMappingIdsToRemove.push(ItemAccessors.getMappingId(item));
|
||||
}
|
||||
}
|
||||
});
|
||||
while (itemMappingIdsToRemove.length > 0) {
|
||||
const itemMappingId = itemMappingIdsToRemove.pop();
|
||||
if (!itemMappingId) {
|
||||
continue;
|
||||
}
|
||||
yield put(characterActions.itemRemove(itemMappingId));
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param choice
|
||||
@@ -2261,3 +2321,40 @@ export function* handlePremadeInfoGet(action) {
|
||||
yield put(characterActions.premadeInfoSetCommit(responseData));
|
||||
}
|
||||
}
|
||||
export function* handleItemPlanInventoryMappingCreate(action) {
|
||||
var _a;
|
||||
const itemDefinitionKey = action.payload.itemDefinitionKey;
|
||||
const containerDefinitionKey = action.payload.containerDefinitionKey;
|
||||
const itemPlanOriginDefinitionKey = (_a = action.payload.itemPlanOriginDefinitionKey) !== null && _a !== void 0 ? _a : null;
|
||||
const quantity = action.payload.quantity;
|
||||
const itemId = Number(DefinitionUtils.getDefinitionKeyId(itemDefinitionKey));
|
||||
const itemEntityTypeId = Number(DefinitionUtils.getDefinitionKeyType(itemDefinitionKey));
|
||||
const containerEntityId = containerDefinitionKey
|
||||
? Number(DefinitionUtils.getDefinitionKeyId(containerDefinitionKey))
|
||||
: null;
|
||||
const containerEntityTypeId = containerDefinitionKey
|
||||
? Number(DefinitionUtils.getDefinitionKeyType(containerDefinitionKey))
|
||||
: null;
|
||||
const originEntityId = itemPlanOriginDefinitionKey
|
||||
? Number(DefinitionUtils.getDefinitionKeyId(itemPlanOriginDefinitionKey))
|
||||
: null;
|
||||
const originEntityTypeId = itemPlanOriginDefinitionKey
|
||||
? Number(DefinitionUtils.getDefinitionKeyType(itemPlanOriginDefinitionKey))
|
||||
: null;
|
||||
if (itemId !== null && itemEntityTypeId !== null) {
|
||||
let params = {
|
||||
equipment: [
|
||||
{
|
||||
containerEntityId,
|
||||
containerEntityTypeId,
|
||||
entityId: itemId,
|
||||
entityTypeId: itemEntityTypeId,
|
||||
quantity,
|
||||
originEntityId,
|
||||
originEntityTypeId,
|
||||
},
|
||||
],
|
||||
};
|
||||
yield call(apiItemsCreate, params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ const SYNC_ACTION_LOOKUP = {
|
||||
[types.CUSTOM_ITEM_CREATE]: true,
|
||||
[types.ITEM_DESTROY]: true,
|
||||
[types.CUSTOM_ITEM_DESTROY]: true,
|
||||
//ITEM PLAN
|
||||
[types.ITEM_PLAN_INVENTORY_MAPPING_CREATE]: true,
|
||||
//CURRENCY
|
||||
[types.CURRENCY_TRANSACTION_SET]: true,
|
||||
//OPTIONAL_FEATURES
|
||||
@@ -454,6 +456,8 @@ export function* executeHandler(action) {
|
||||
[types.CUSTOM_ITEM_DESTROY]: sagaHandlers.handleCustomItemDestroy,
|
||||
[types.ITEM_CHARGES_SET]: sagaHandlers.handleItemChargesSet,
|
||||
[types.ITEM_MOVE_SET]: sagaHandlers.handleItemMove,
|
||||
//ITEM PLAN
|
||||
[types.ITEM_PLAN_INVENTORY_MAPPING_CREATE]: sagaHandlers.handleItemPlanInventoryMappingCreate,
|
||||
//CURRENCY
|
||||
[types.CURRENCY_TRANSACTION_SET]: sagaHandlers.handleCurrencyTransactionSet,
|
||||
//OPTIONAL_FEATURES
|
||||
|
||||
@@ -2,11 +2,10 @@ import { call, put, select } from 'redux-saga/effects';
|
||||
import { characterActions, serviceDataActions } from '../../actions';
|
||||
import { ApiRequests } from '../../api';
|
||||
import * as apiShared from '../../api/requests';
|
||||
import { CampaignAccessors, PartyInventorySharingStateEnum } from '../../engine/Campaign';
|
||||
import { CampaignAccessors } from '../../engine/Campaign';
|
||||
import { CreatureAccessors } from '../../engine/Creature';
|
||||
import { DefinitionAccessors, DefinitionTypeEnum, DefinitionUtils } from '../../engine/Definition';
|
||||
import { DefinitionPoolUtils } from '../../engine/DefinitionPool';
|
||||
import { FeatureFlagEnum, FeatureFlagInfoUtils } from '../../engine/FeatureFlagInfo';
|
||||
import { HelperUtils } from '../../engine/Helper';
|
||||
import { InfusionAccessors, InfusionTypeEnum } from '../../engine/Infusion';
|
||||
import { InfusionChoiceAccessors } from '../../engine/InfusionChoice';
|
||||
@@ -17,7 +16,7 @@ import { VehicleAccessors, VehicleSimulators } from '../../engine/Vehicle';
|
||||
import { VehicleComponentAccessors } from '../../engine/VehicleComponent';
|
||||
import { initialCoinState } from '../../reducers/character';
|
||||
import * as SagaHelpers from '../../sagas/SagaHelpers';
|
||||
import { characterSelectors, featureFlagInfoSelectors, rulesEngineSelectors, serviceDataSelectors, } from '../../selectors';
|
||||
import { characterSelectors, rulesEngineSelectors, serviceDataSelectors, } from '../../selectors';
|
||||
import { TypeScriptUtils } from '../../utils';
|
||||
import { callCommitAction } from '../../utils/ReduxActionUtils';
|
||||
import { apiCreatureCreate, apiItemsCreate, handleLoadDefinitions } from '../character/handlers';
|
||||
@@ -95,6 +94,8 @@ export function* handleInfusionCreate(action) {
|
||||
entityId: itemId,
|
||||
entityTypeId: itemEntityTypeId,
|
||||
quantity: 1,
|
||||
originEntityId: null,
|
||||
originEntityTypeId: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -165,14 +166,15 @@ export function* handleInfusionDestroy(action) {
|
||||
//if infused item is a REPLICATE type remove the item from inventory
|
||||
//otherwise leave the item but un-attune and reset charges
|
||||
if (infusionType === InfusionTypeEnum.REPLICATE && inventoryMappingId !== null) {
|
||||
yield put(characterActions.itemRemove(inventoryMappingId, true));
|
||||
yield put(characterActions.itemDestroy(inventoryMappingId, true, action.meta.accept, action.meta.reject));
|
||||
// Mapping was removed on the backend due to being an item
|
||||
// Only call commit action to mirror backend
|
||||
yield put(callCommitAction(serviceDataActions.infusionMappingRemove, action.payload.infusionId, action.payload.inventoryMappingId));
|
||||
}
|
||||
else {
|
||||
const inventoryLookup = yield select(rulesEngineSelectors.getInventoryLookup);
|
||||
const foundItem = HelperUtils.lookupDataOrFallback(inventoryLookup, action.payload.inventoryMappingId);
|
||||
const partyInventoryLookup = yield select(rulesEngineSelectors.getPartyInventoryLookup);
|
||||
const foundItem = HelperUtils.lookupDataOrFallback(Object.assign(Object.assign({}, inventoryLookup), partyInventoryLookup), action.payload.inventoryMappingId);
|
||||
if (foundItem) {
|
||||
if (ItemAccessors.isAttuned(foundItem)) {
|
||||
yield put(characterActions.itemAttuneSet(inventoryMappingId, false));
|
||||
@@ -182,6 +184,9 @@ export function* handleInfusionDestroy(action) {
|
||||
// TODO IMS Infusions come back here and switch this out
|
||||
yield put(characterActions.itemChargesSet(ItemAccessors.getMappingId(foundItem), 0));
|
||||
}
|
||||
if (action.meta.accept) {
|
||||
action.meta.accept();
|
||||
}
|
||||
}
|
||||
yield put(serviceDataActions.infusionMappingRemove(action.payload.infusionId, action.payload.inventoryMappingId));
|
||||
}
|
||||
@@ -244,31 +249,27 @@ export function* handleVehicleRemove(action) {
|
||||
}
|
||||
export function* handlePartyInventoryRequest(action) {
|
||||
var _a, _b, _c, _d, _e, _f;
|
||||
const featureFlagInfo = yield select(featureFlagInfoSelectors.getFeatureFlagInfo);
|
||||
const partyFlag = FeatureFlagInfoUtils.getFeatureFlagInfoValue(FeatureFlagEnum.RELEASE_GATE_IMS, featureFlagInfo);
|
||||
// Party Inventory
|
||||
if (partyFlag) {
|
||||
const campaignInfo = yield select(characterSelectors.getCampaign);
|
||||
if (campaignInfo) {
|
||||
const data = yield call(SagaHelpers.getApiRequestData, apiShared.getPartyInventory, { campaignId: CampaignAccessors.getId(campaignInfo) });
|
||||
// This sets the data in the serviceData.partyInfo state
|
||||
yield put(serviceDataActions.partyCampaignInfoSet(Object.assign(Object.assign({}, campaignInfo), { sharingState: (_a = data === null || data === void 0 ? void 0 : data.sharingState) !== null && _a !== void 0 ? _a : PartyInventorySharingStateEnum.OFF, partyInventory: (_b = data === null || data === void 0 ? void 0 : data.partyItems) !== null && _b !== void 0 ? _b : [], spells: (_c = data === null || data === void 0 ? void 0 : data.spells) !== null && _c !== void 0 ? _c : null, modifiers: (_d = data === null || data === void 0 ? void 0 : data.modifiers) !== null && _d !== void 0 ? _d : null, coin: (_e = data === null || data === void 0 ? void 0 : data.currency) !== null && _e !== void 0 ? _e : initialCoinState })));
|
||||
yield call(handleUpdatePartyInventoryValues, data);
|
||||
// Party infusions set into serviceData
|
||||
if ((_f = data.partyInfusions) === null || _f === void 0 ? void 0 : _f.length) {
|
||||
let infusionDefinitionIds = new Set();
|
||||
for (let i = 0; i < data.partyInfusions.length; i++) {
|
||||
const mapping = data.partyInfusions[i];
|
||||
yield put(callCommitAction(serviceDataActions.infusionMappingAdd, mapping));
|
||||
const definitionKey = InfusionAccessors.getDefinitionKey(mapping);
|
||||
if (definitionKey !== null) {
|
||||
infusionDefinitionIds.add(DefinitionUtils.getDefinitionKeyId(definitionKey));
|
||||
}
|
||||
}
|
||||
if (infusionDefinitionIds.size > 0) {
|
||||
yield call(handleLoadDefinitions, DefinitionTypeEnum.INFUSION, Array.from(infusionDefinitionIds));
|
||||
const campaignInfo = yield select(characterSelectors.getCampaign);
|
||||
if (campaignInfo) {
|
||||
const data = yield call(SagaHelpers.getApiRequestData, apiShared.getPartyInventory, { campaignId: CampaignAccessors.getId(campaignInfo) });
|
||||
// This sets the data in the serviceData.partyInfo state
|
||||
yield put(serviceDataActions.partyCampaignInfoSet(Object.assign(Object.assign({}, campaignInfo), { partyInventory: (_a = data === null || data === void 0 ? void 0 : data.partyItems) !== null && _a !== void 0 ? _a : [], spells: (_b = data === null || data === void 0 ? void 0 : data.spells) !== null && _b !== void 0 ? _b : null, modifiers: (_c = data === null || data === void 0 ? void 0 : data.modifiers) !== null && _c !== void 0 ? _c : null, coin: (_d = data === null || data === void 0 ? void 0 : data.currency) !== null && _d !== void 0 ? _d : initialCoinState, partyRestrictions: (_e = data === null || data === void 0 ? void 0 : data.partyRestrictions) !== null && _e !== void 0 ? _e : [] })));
|
||||
yield call(handleUpdatePartyInventoryValues, data);
|
||||
// Party infusions set into serviceData
|
||||
if ((_f = data.partyInfusions) === null || _f === void 0 ? void 0 : _f.length) {
|
||||
let infusionDefinitionIds = new Set();
|
||||
for (let i = 0; i < data.partyInfusions.length; i++) {
|
||||
const mapping = data.partyInfusions[i];
|
||||
yield put(callCommitAction(serviceDataActions.infusionMappingAdd, mapping));
|
||||
const definitionKey = InfusionAccessors.getDefinitionKey(mapping);
|
||||
if (definitionKey !== null) {
|
||||
infusionDefinitionIds.add(DefinitionUtils.getDefinitionKeyId(definitionKey));
|
||||
}
|
||||
}
|
||||
if (infusionDefinitionIds.size > 0) {
|
||||
yield call(handleLoadDefinitions, DefinitionTypeEnum.INFUSION, Array.from(infusionDefinitionIds));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user