diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3b76627 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +build: + docker build -t cr.aaaaa.news/lto:latest . +push: + docker push cr.aaaaa.news/lto:latest + + diff --git a/src/App.vue b/src/App.vue index d230c8d..26ceebb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,9 +8,9 @@ import OrderDisplay from "./components/OrderDisplay.vue"; loadStore() @@ -35,7 +35,6 @@ loadStore() text-align: center; color: #2c3e50; height: 100vh; - overflow-y: hidden; } .handsontable th { border-right: 0px !important; @@ -84,6 +83,7 @@ loadStore() grid-column-gap: 0px; grid-row-gap: 0px; overflow: hidden; + display: scroll; } .splash { diff --git a/src/components/CharacterCard.vue b/src/components/CharacterCard.vue index 5c51660..94b79e0 100644 --- a/src/components/CharacterCard.vue +++ b/src/components/CharacterCard.vue @@ -55,13 +55,11 @@ const selectCharacter = () => { diff --git a/src/components/CharacterInventory.vue b/src/components/CharacterInventory.vue index a4b43a8..591fef7 100644 --- a/src/components/CharacterInventory.vue +++ b/src/components/CharacterInventory.vue @@ -55,6 +55,7 @@ const send_orders = () => { const origin = activeTable const pending:OrderDetails[] = []; for(const row of dat) { + try{ const nm = Number(row[idxNumber].replace("x","")) const target = (row[idxTarget] as string).replaceAll("-","").trim() if(!isNaN(nm) && nm > 0 && target.length > 0){ @@ -66,6 +67,8 @@ const send_orders = () => { } pending.push(info) } + }catch(e){ + } } log.debug("OrderDetails", pending) for(const d of pending){ diff --git a/src/components/CharacterRoulette.vue b/src/components/CharacterRoulette.vue index f173622..418b5f7 100644 --- a/src/components/CharacterRoulette.vue +++ b/src/components/CharacterRoulette.vue @@ -48,7 +48,7 @@ import { saveStore, useStoreRef } from '../state/state'; #character_roulette { display: flex; flex-direction: row; - justify-content: center; + justify-content: normal; align-items: center; overflow-x: scroll; width: 1000px; diff --git a/src/lib/lifeto/api.ts b/src/lib/lifeto/api.ts index ee1c983..227d77e 100644 --- a/src/lib/lifeto/api.ts +++ b/src/lib/lifeto/api.ts @@ -3,7 +3,7 @@ import { TricksterAccount, TricksterInventory } from "../trickster" import { v4 as uuidv4 } from 'uuid'; import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; -export const BankEndpoints = ["internal-xfer-item", "bank-item"] as const +export const BankEndpoints = ["internal-xfer-item", "bank-item", "sell-item","buy-from-order","cancel-order"] as const export type BankEndpoint = typeof BankEndpoints[number] export interface LTOApi { diff --git a/src/lib/lifeto/lifeto.ts b/src/lib/lifeto/lifeto.ts index 8c0602d..87f7bb2 100644 --- a/src/lib/lifeto/lifeto.ts +++ b/src/lib/lifeto/lifeto.ts @@ -1,14 +1,14 @@ -import { Axios, AxiosResponse } from "axios" -import log from "loglevel" -import { bank_endpoint, Session } from "../session" +import { Axios, AxiosResponse, Method } from "axios" +import log, { debug } from "loglevel" +import { bank_endpoint, EndpointCreator, market_endpoint, Session } from "../session" import { dummyChar, TricksterAccount, TricksterInventory, TricksterItem, TricksterWallet } from "../trickster" import { BankEndpoint, LTOApi } from "./api" export const pathIsBank = (path:string):boolean => { - if(!path.includes("/")) { - return true + if(path.includes("/")) { + return false } - return false + return true } export const splitPath = (path:string):[string,string]=>{ @@ -28,9 +28,19 @@ export class LTOApiv0 implements LTOApi { } BankAction = async (e: BankEndpoint, t: T):Promise => { - return this.s.request("POST",e,t,bank_endpoint).then((x)=>{ - return x.data - }) + let VERB:Method | "POSTFORM" = "POST" + let endpoint:EndpointCreator = bank_endpoint + switch(e){ + case "buy-from-order": + case "cancel-order": + endpoint = market_endpoint + case "sell-item": + VERB = "POSTFORM" + default: + } + return this.s.request(VERB as any,e,t,endpoint).then((x)=>{ + return x.data + }) } GetInventory = async (char_path: string):Promise =>{ if(char_path.startsWith(":")) { @@ -42,15 +52,16 @@ export class LTOApiv0 implements LTOApi { let name = "bank" let id = 0 let galders = 0 - if(pathIsBank("char_path")){ + if(pathIsBank(char_path)){ let [char, val] = Object.entries(o.characters)[0] as [string,any] name = val.name id = Number(char) - galders = val.galders as number + galders = 0 }else { - id = o.id - name = o.name - galders = o.galders + let [char, val] = Object.entries(o.characters)[0] as [string,any] + name = val.name + id = Number(char) + galders = val.galders } let out = { name, @@ -71,8 +82,9 @@ export class LTOApiv0 implements LTOApi { return ans.data.map((x:any)=>{ return { name: x.name, - characters: [{id: x.id, path:x.name, name: x.name+'/bank', class:-8, base_job: -8, current_job: -8},...Object.values(x.characters).map((z:any)=>{ + characters: [{id: x.id,account_id:x.id, path:x.name, name: x.name+'/bank', class:-8, base_job: -8, current_job: -8},...Object.values(x.characters).map((z:any)=>{ return { + account_id: x.id, id: z.id, name: z.name, path: x.name+"/"+z.name, diff --git a/src/lib/lifeto/order.ts b/src/lib/lifeto/order.ts index ff7e2a4..84cd6b6 100644 --- a/src/lib/lifeto/order.ts +++ b/src/lib/lifeto/order.ts @@ -15,6 +15,9 @@ export interface TxnDetails { origin_path:string target_path:string + + origin_account:string + target_account:string } @@ -207,7 +210,7 @@ export class BankItem extends BasicOrder{ origin_item.item_count = origin_item.item_count - this.details?.count! r.dirty.value++ }else { - throw x.msg ? "unknown error" : "" + throw x.msg ? x.msg : "unknown error" } }) .catch((e)=>{ @@ -224,3 +227,244 @@ export class BankItem extends BasicOrder{ return this } } + + +export interface PrivateMarketRequest { + item_uid:string + qty:string + account:string + currency:string + price:number + private:number +} + +export interface PrivateMarketResponse extends BasicResponse {} + +export class PrivateMarket extends BasicOrder{ + order_type = "PrivateMarket"; + + originalRequest:PrivateMarketRequest + originalResponse?:PrivateMarketResponse + + listingId?: string + listingHash?: string + + constructor(details:TxnDetails) { + super(details) + this.originalRequest = { + item_uid: details.item_uid, + qty: details.count.toString(), + account: details.origin_account, + private: 1, + currency: "0", + price: 1, + } + } + async tick(r:RefStore, api:LTOApi):Promise { + if(this.state !== "PENDING" ){ + return + } + this.mark("WORKING") + return api.BankAction("sell-item",this.originalRequest) + .then((x)=>{ + debug("PrivateMarket",x) + if(x.status == 200){ + this.stage = 1 + this.originalResponse = x + this.mark("SUCCESS") + this.listingId = x.data["listing_id"] + this.listingHash = x.data["hash"] + try{ + const origin_item = r.invs.value.get(this.details?.origin_path!)!.items[this.details?.item_uid!]! + origin_item.item_count = origin_item.item_count - this.details?.count! + r.dirty.value++ + }catch(e){ + } + }else { + throw x.msg ? x.msg : "unknown error" + } + }) + .catch((e)=>{ + this.stage = 1 + this.err = e + this.mark("ERROR") + }) + } + + parse(i:any):PrivateMarket { + super.parse(i) + this.originalRequest = i.originalRequest + this.originalResponse = i.originalResponse + this.listingId = i.listingId + this.listingHash = i.listingHash + return this + } +} + + +export interface MarketMoveRequest { + listing_id?: string + qty:string + account:string + character: string +} + +export interface MarketMoveResponse extends BasicResponse { + item_uid: string +} + + +export class MarketMove extends PrivateMarket { + order_type = "MarketMove"; + + moveRequest:MarketMoveRequest + moveResponse?:MarketMoveResponse + + moveStage:number + moveState: TxnState + + newUid: string + + constructor(details:TxnDetails) { + super(details) + this.moveStage = 0 + this.moveState = "PENDING" + this.newUid = "" + this.moveRequest = { + qty: details.count.toString(), + account: details.target_account, + character: (details.target_path.includes("/")) ? details.target : "0" , + listing_id: "", // not initially populated + } + } + async tick(r:RefStore, api:LTOApi):Promise { + try { + await super.tick(r, api) + }catch(e){ + return + } + switch(super.status()) { + case "SUCCESS": + break; + case "ERROR": + this.moveState = "ERROR" + return + default: + return + } + if(this.moveState !== "PENDING" ){ + return + } + this.moveRequest.listing_id = `${this.listingId}-${this.listingHash}` + this.moveState = "WORKING" + return api.BankAction("buy-from-order",this.moveRequest) + .then((x)=>{ + debug("MarketMove",x) + this.moveResponse = x + if(x.status == 200){ + this.moveStage = 1 + this.moveState = "SUCCESS" + this.newUid = x.item_uid + r.dirty.value++ + }else { + throw x ? x : "unknown error" + } + }) + .catch((e)=>{ + this.moveStage = 1 + this.err = e + this.moveState = "ERROR" + }) + } + + progress():[number,number]{ + return [this.stage + this.moveStage, 2] + } + status():string { + return this.moveState + } + + parse(i:any):MarketMove { + super.parse(i) + this.moveRequest = i.moveRequest + this.moveResponse = i.moveResponse + this.moveState = i.moveState + this.moveStage = i.moveStage + return this + } +} + +export class MarketMoveToChar extends MarketMove { + order_type = "MarketMoveToChar"; + + charRequest:InternalXferRequest + charResponse?:InternalXferResponse + + charStage:number + charState: TxnState + + constructor(details:TxnDetails) { + super(details) + this.charStage = 0 + this.charState = "PENDING" + this.charRequest = { + item_uid: "", + qty: details.count.toString(), + new_char: details.target, + account: details.target_account, + } + } + async tick(r:RefStore, api:LTOApi):Promise { + try { + await super.tick(r, api) + }catch(e){ + return + } + switch(super.status()) { + case "SUCCESS": + break; + case "ERROR": + this.charState = "ERROR" + return + default: + return + } + if(this.charState !== "PENDING" ){ + return + } + this.charState = "WORKING" + this.charRequest.item_uid = this.newUid + return api.BankAction("internal-xfer-item",this.charRequest) + .then((x)=>{ + debug("MarketMoveToChar",x) + this.charResponse = x + if(x.status == 200){ + this.charStage = 1 + this.charState = "SUCCESS" + }else { + throw x ? x : "unknown error" + } + }) + .catch((e)=>{ + this.charStage = 1 + this.err = e + this.charState = "ERROR" + }) + } + + progress():[number,number]{ + return [this.stage +this.moveStage+ this.charStage, 3] + } + status():string { + return this.charState + } + + parse(i:any):MarketMoveToChar { + super.parse(i) + this.charRequest = i.charRequest + this.charResponse = i.charResponse + this.charState = i.charState + this.charStage = i.charStage + return this + } +} diff --git a/src/lib/lifeto/order_manager.ts b/src/lib/lifeto/order_manager.ts index 5a71d94..ada9212 100644 --- a/src/lib/lifeto/order_manager.ts +++ b/src/lib/lifeto/order_manager.ts @@ -1,10 +1,8 @@ -import log from "loglevel"; -import { Ref } from "vue"; import { RefStore } from "../../state/state"; -import { JsonLoadable, Serializable } from "../storage"; +import { Serializable } from "../storage"; import { LTOApi } from "./api"; import { pathIsBank, splitPath } from "./lifeto"; -import { BankItem, BankXfer, InternalXfer, InvalidOrder, Order, TxnDetails } from "./order"; +import { BankItem, InternalXfer, InvalidOrder, MarketMove, Order,MarketMoveToChar, TxnDetails } from "./order"; export interface OrderDetails { item_uid: string | "galders" @@ -23,7 +21,7 @@ export class OrderTracker implements Serializable { let hasDirty = false console.log("ticking") for(const [id, order] of Object.entries(this.orders)) { - if(order.state == "SUCCESS" || order.state == "ERROR") { + if(order.status() == "SUCCESS" || order.status() == "ERROR") { console.log("finished order", order) hasDirty = true delete this.orders[id] @@ -49,7 +47,7 @@ export class OrderTracker implements Serializable { let newOrder:Order | undefined = undefined console.log("loading", o) if(o.details){ - if(o.state == "SUCCESS" || o.state == "ERROR") { + if(o.status() == "SUCCESS" || o.status() == "ERROR") { continue } switch(o.order_type) { @@ -59,6 +57,11 @@ export class OrderTracker implements Serializable { case "BankItem": newOrder = new BankItem(o.details).parse(o) break; + case "MarketMove": + newOrder = new MarketMove(o.details).parse(o) + case "MarketMoveToChar": + newOrder = new MarketMoveToChar(o.details).parse(o) + break; case "InvalidOrder": newOrder = new InvalidOrder("").parse(o) break; @@ -106,7 +109,10 @@ export class OrderSender { bank_to_bank(o:OrderDetails): Order{ const origin = this.r.chars.value.get(o.origin_path) const target = this.r.chars.value.get(o.target_path) - return notSupported + if(!(origin && target)) { + return notFound + } + return new MarketMove(this.transformInternalOrder(o)) } bank_to_user(o:OrderDetails): Order{ // get the uid of the bank @@ -117,9 +123,9 @@ export class OrderSender { } const [account, name] = splitPath(target.path) if(account != origin.path) { - return notSupported + return new MarketMoveToChar(this.transformInternalOrder(o)) } - return new InternalXfer(this.transformOrder(o)) + return new InternalXfer(this.transformInternalOrder(o)) } user_to_bank(o:OrderDetails): Order{ const origin = this.r.chars.value.get(o.origin_path) @@ -129,17 +135,20 @@ export class OrderSender { } const [account, name] = splitPath(origin.path) if(account != target.path) { - return notSupported + return new MarketMove(this.transformInternalOrder(o)) } - return new BankItem(this.transformOrder(o)) + return new BankItem(this.transformInternalOrder(o)) } user_to_user(o:OrderDetails): Order{ const origin = this.r.chars.value.get(o.origin_path) const target = this.r.chars.value.get(o.target_path) - return notSupported + if(!(origin && target)) { + return notFound + } + return new MarketMoveToChar(this.transformInternalOrder(o)) } - transformOrder(o:OrderDetails):TxnDetails { + private transformInternalOrder(o:OrderDetails):TxnDetails { const origin = this.r.chars.value.get(o.origin_path)! const target = this.r.chars.value.get(o.target_path)! return { @@ -149,6 +158,8 @@ export class OrderSender { count: o.count, origin_path: o.origin_path, target_path: o.target_path, + origin_account: origin.account_id.toString(), + target_account: target.account_id.toString(), } } } diff --git a/src/lib/session.ts b/src/lib/session.ts index 88b8546..93a3fff 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -7,6 +7,7 @@ export const SITE_ROOT = "/lifeto/" export const API_ROOT = "api/lifeto/" export const BANK_ROOT = "item-manager-action/" +export const MARKET_ROOT = "marketplace-api/" const login_endpoint = (name:string)=>{ return SITE_ROOT + name @@ -18,12 +19,17 @@ export const bank_endpoint = (name:string):string =>{ return SITE_ROOT+BANK_ROOT + name } +export const market_endpoint = (name:string):string =>{ + return SITE_ROOT+MARKET_ROOT+ name +} + export const EndpointCreators = [ api_endpoint, bank_endpoint, + market_endpoint, ] -type EndpointCreator = typeof EndpointCreators[number] +export type EndpointCreator = typeof EndpointCreators[number] export interface Session { user:string @@ -32,8 +38,6 @@ export interface Session { request:(verb:Method,url:string,data:any,c?:EndpointCreator)=>Promise } - - export class LoginHelper { user:string pass:string @@ -66,7 +70,7 @@ export class LogoutHelper{ return axios.get(login_endpoint("logout"),{withCredentials:false}).catch((e)=>{}) } } -const sleep= async(ms:number)=> { +const sleep = async(ms:number)=> { return new Promise(resolve => setTimeout(resolve, ms)) } @@ -86,6 +90,9 @@ export class TokenSession implements Session { case "post": promise = axios.post(c(url),data,this.genHeaders()) break; + case "postform": + promise = axios.postForm(c(url),data) + break; case "postraw": const querystring = qs.stringify(data) promise = axios.post(c(url),querystring,this.genHeaders()) diff --git a/src/lib/trickster.ts b/src/lib/trickster.ts index f7618cb..790fae9 100644 --- a/src/lib/trickster.ts +++ b/src/lib/trickster.ts @@ -28,6 +28,7 @@ export interface TricksterAccount { } export interface Identifier { + account_id: number id: number name: string path: string